0

Nexus SのNFCでFelicaのIDを読む(2)

Nexus SのNFCでFelicaのIDを読む – 橋本詳解の時に、Android 2.3 GingerBread NFCをやってみる – TOPGATE Google関連技術サイトのTagWrapper.javaを使わせてもらってたんだけど先日のNexus Sのandroid2.3.4へのアップデートで動かなくなったので少しやり方変えた。
TagWrapper.javaの中でやってる事を参考にさせてもらった。


ただFelicaのユニークなIDだけが必要だったらこれでいい
(先に橋本詳解の方に書いたManifestとかguavaとかを設定しておく必要はある)

import java.lang.reflect.*;
import java.util.*;

import com.google.common.primitives.UnsignedBytes;

import android.app.Activity;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.*;
たくさんのimportが必要。


    public void onCreate(Bundle savedInstanceState) {        
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
resolveIntent(this.getIntent());
}

void resolveIntent(Intent intent) {
if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
try{
Parcelable tag = intent.getParcelableExtra("android.nfc.extra.TAG");
Field f = tag.getClass().getDeclaredField("mId");
f.setAccessible(true);
byte[] mId = (byte[]) f.get(tag);
StringBuilder sb = new StringBuilder();
for (byte id : mId) {
String hexString = Integer.toHexString(UnsignedBytes.toInt(id));
if (hexString.length() == 1) sb.append("0");
sb.append(hexString);
}
String id = sb.toString();
Log.v("TAG", id);
}
catch(Exception e){
e.printStackTrace();
}
}
}

0

glitchtweet.com作った

2ヶ月ぐらい前に作った。blog書くのめんどくさくて放置してた。

http://glitchtweet.com


俺がやっているような素敵な装飾がついたツイートがだれでもできるwebサービスです。しかも気に入ったのがでるまで何度でもやりなおせる。
iPhoneやAndroidから使うことを想定している。



ライブラリ以外のコードを数えたら、286行しかなかった。
その他は以前作ったテキストに文字装飾を行うglitchtext.jsが500行ぐらい。sinatra、haml、jqueryのおかげでシンプルに書けた。

ソースはここにある
shokai/glitchtweet-web-app – GitHub



以下細かいことなどを書く

■ファイルサイズ
でかい。glitchtext.jsが辞書が巨大すぎて300kb以上ある。けどしょうがないし、まあキャッシュ効くからいいか


■サーバー
さくらVPSのdev.shokai.orgに、virtualhostでglitchtweet.com割り当てて使ってる。


■開発環境
まずglitchtext.jsはv8とRakeで単体でテストをしている。前に書いた
glitchtweet.comのwebアプリ自体はローカルでwebrickで起動して、chromeの開発パネルでjsなどのデバッグをした。あとはiPhoneとAndroidのsafariのブラウザで開いて、見た目を調整した。


■なるべく画面遷移しない
jquery mobileやjQTouchを使うとネイティブアプリ風に外見で、横にスライドして表示を切り替えるwebアプリを作れるけど
面倒だったので使わなかった。visibleをon/offしてtweetボタンを表示したりしなかったり等している。
なのでviewのテンプレートは1つしかない。


■twitterログイン
生成したglitchテキストはoAuthでログインしてtweetされるが、ユーザ情報管理にDBは使っていない。
cookieにtwitterのoauth tokenとsecretを保存している。
最初の画面でログイン済みかどうかは、haml template上でtokenとsecretがあるかどうかだけで表示を分岐しているので、実際glitchtweet.com側では誰がどんなtweetをしているかは把握していない(しようと思えばできる)


■cookieでsession
sinatraでcookieベースのsessionを使うようにしている。
Rack::Session::Cookieを使う – 橋本詳解に書いた。
glitchtweet.comでは2週間cookieで保存するようにしている。
use Rack::Sessioin::Cookieしたらsinatraのsession関数がcookieを使うようになってくれた。
(これで大丈夫ですよね?)


■ホーム画面アイコン

<link href='http://glitchtweet.com/img/icon.png' rel='apple-touch-icon' />
これをheadに書いておくと、iPhoneのホーム画面にブックマーク保存するとアイコンが付く。
Android2.3でもホーム画面にブックマークショートカットを作ったらアイコンが出るようになった。しかも角丸化される。apple-touch-iconって名前なのに処理してくれるAndroidえらい。


■テスト環境にwebrick使うようにした
開発中にSinatra1.2.1が出たのでupdateしたら、thinを使うとhttpリクエスト送った瞬間に問答無用で強制終了するようになった。
webrick使うようにした。
Sinatra1.2.1とthin1.2.10を同時に使うと死ぬ – 橋本詳解


■viewport
@hitoriblogさんに教えてもらった。
適当なhtml/cssを書いていてもiPhoneではそれなりに正しいサイズで表示されるんだけど、Androidではテキストエリアをクリックした瞬間にものすごいズームをされてしまう。
これはviewportをheadに指定しておくとなんとかなる。Androidはデバイスがたくさんあるので、画面サイズが微妙に違うのでwebサービス作ってる人は大変そう。
<meta content='width=device-width, user-scalable=no' name='viewport' /> 
このへんも参考になる。


■shakeイベント
ネイティブアプリではshakeのイベント、つまりiPhone本体を振ったイベントを取得して、undoに使われている。
全く、操作と実行内容の関連付けが最も意味不明な機能だと思う。

でもiOS4.2からsafariでも加速度センサーが使えるようになったので、せっかくだからshakeイベントを取れるjsライブラリを作った。
shokai/js-iphone-shake-event – GitHub

glitchtweet.comでも使っている。tweet後に、もう一度同じソースからglitchしたい時にshakeすると消えた文字が復元される。

これについてはあとで書く。


■iphone-js-console
shokai/iphone-js-console – GitHub
iphone-shake-event.jsを作っている時に、iPhone実機でのデバッグをする為に作った。
パソコンのterminal上でjsを書くと、iPhone上で実行されたり、パソコン上でiPhoneのブラウザ上のjsの値を読み出したりできて便利。

これについてもあとで書く。

1

PhoneGapでAndroidアプリを作る

増井さんがPhoneGapを使って5分で作るAndroidアプリケーションというビデオを2時間ぐらいかけて撮影していたので、俺も触発されてPhoneGapやってみた。
PhoneGapはHTML/CSS/JavaScriptでAndroidやiPhoneアプリを作れる環境。アプリ起動時にブラウザコンポーネントを読み込んで、そこにHTMLで画面を作るしくみになってる。


特にコンテンツとして面白いところもないサンプルアプリケーションっぽいのができた。バスの中で。
せっかくAndroidのネイティブアプリ相当が作れるのだから、加速度センサーを秒間30回取得して画面を描画しつつ、本体のバイブレーションとビープを鳴らしたりした。



ソースはgithubに全部置いておいた
https://github.com/shokai/droidgap-test-app


さて、PhoneGapでAndroidアプリを作る方法をメモしておく。


■Androidの開発環境を作る
まず、普通にEclipseでJavaとXMLで書かれたAndroidプロジェクトをコンパイルできる環境を整える。
android sdkインストール実機で実行するに書いた。
Javaは書かなくていいので、最初に新規プロジェクト作成したままをコンパイルして、自分のAndroid端末(もしくはエミュレータ)に送って実行できる事を確かめておく。



■PhoneGap/DroidGapでの開発環境を作る
droidgapでandroidアプリ開発にくわしく書いた。
PhoneGap本家サイトからPhoneGap一式をダウンロードすると、iPhoneとかBlackberryとかpalm用の環境も手に入るけど、DroidGapが入ってない。
DroidGapはAndroid Development Toolと連携してプロジェクトを自動生成してPhoneGapでの開発に必要なファイルも全て配置してくれる神ツールなので、DroidGapを使うべき。DroidGap自体もrubyで書かれているので不具合があってもなんとかしやすい。

https://github.com/phonegap/phonegapではなくhttps://github.com/phonegap/phonegap-androidの方を使う。



■アプリを作る

droidgap gen アプリ名
してテンプレートを作って、
ant install
でビルドしてインストールできる。アプリの実行は端末から手動でする。


最近はhamlが気に入っているので、index.hamlを書く
!!! XML
%html
%head
%script{:type => 'text/javascript', :src => "./phonegap.js"}
%script{:type => 'text/javascript', :src => "./jquery.js"}
%script{:type => 'text/javascript', :src => "./jquery.color.ver2.js"}
%script{:type => 'text/javascript', :src => "./main.js"}
%body
%h1 droidgap test app
%h2 accelerometer:
%div#acc_vars
%div#x
%div#y
%div#z
%div#color
%h2 logs
%div#log

で、hamlからhtmlに変換する
gem install haml
haml index.haml index.html


中のコンテンツはjQueryで動的に作るので、idの振られたdivがあるだけでhtmlはシンプル。
index.html
<?xml version='1.0' encoding='utf-8' ?>
<html>
<head>
<script src='./phonegap.js' type='text/javascript'></script>
<script src='./jquery.js' type='text/javascript'></script>
<script src='./jquery.color.ver2.js' type='text/javascript'></script>
<script src='./main.js' type='text/javascript'></script>
</head>
<body>
<h1>droidgap test app</h1>
<h2>accelerometer:</h2>
<div id='acc_vars'>
<div id='x'></div>
<div id='y'></div>
<div id='z'></div>
</div>
<div id='color'></div>
<h2>logs</h2>
<div id='log'></div>
</body>
</html>


jQueryは自分でダウンロードしてきて同じディレクトリに置いておく。
phonegap.jsはdroidgap gemした時に自動配置してくれてる。
で、main.jsを書く。
var acc_stat = false;
var acc_watch;
var acc_max = 0;

document.addEventListener("deviceready", function(){
navigator.notification.beep(1);
navigator.notification.vibrate(0);
accel(true);
}, true);

$(function(){
log('start');
$('div#color').css('width','100%').css('height',400).css('background-color', '#000000');
$('div#color').click(function(){
navigator.notification.beep(1);
navigator.notification.vibrate(0);
});
});

function log(message){
$('div#log').prepend($('<p>').html(message));
};

function accel(start_stop){
log("accel "+start_stop);
if(start_stop == acc_stat) return;
acc_stat = start_stop;
if(start_stop){
var opts = new Object();
opts.frequency = 30; // 30ミリ秒
acc_watch = navigator.accelerometer.watchAcceleration(
display_acc, function(e){
acc_stat = false;
navigator.accelerometer.clearWatch(acc_watch);
log("accelerometer error "+e.name+":"+e.message);
}, opts);
}
else{
navigation.accelerometer.clearWatch(acc_watch);
display_acc(new Object());
}
};

function display_acc(acc){
$('div#acc_vars div#x').html('x : '+acc.x);
$('div#acc_vars div#y').html('y : '+acc.y);
$('div#acc_vars div#z').html('z : '+acc.z);
var r = Math.floor(acc.x*60);
if(r < 0) r *= -1;
if(r > 255) r = 255;
var g = Math.floor(acc.y*60);
if(g < 0) g *= -1;
if(g > 255) g = 255;
var b = Math.floor(acc.z*60);
if(b < 0) b *= -1;
if(b > 255) b = 255;
$('div#color').css('background-color',$.parseColorCode([r,g,b]));
};
30ミリ秒毎に加速度センサーの値を取得して、毎回HTMLを書きなおしているけど特に問題ない。
あと、ハードウェアの機能が使えるようになるタイミングはdocument.onloadではなく、devicereadyイベントの後になる。
ユーザが画面に触れて動かす系のイベントは、ふつうに$(‘dom’).click()とかで登録して良いだろうけど、起動時に加速度センサーをonにする等の処理はdevicereadyイベントに登録する。

色を書き換えるのに、jQuery Color プラグインを拡張:humming birdを使わせてもらった。0〜255の値から16進数のカラーコードを作れるのでとても便利。



■JavaScriptのデバッグ
Androidのwebkitはchromeやfirebugと同じくconsole.log(message)関数が使えて、logcatで見れる。JavaScriptの実行時エラーも見れる。
Debugging Web Apps | Android Developers

PhoneGapの場合は行頭にI/Web Consoleが付いてconsole.logが、E/Web Consoleでエラーが来るのでgrepしておくと良い
adb logcat | grep "^./Web Console" --color=auto

Androidのデフォルトのwebブラウザも、console.logとエラーをadbに投げてくれる。こちらは^./browserでgrep。
adb logcat | grep "^./browser" --color=auto

このようにどのファイルの何行目でどんなエラーが起こったかまでちゃんと追えるので便利


■PhoneGapができなそうな事をいくつか
アプリ組み込み版のGoogleMapが表示できなそう。PhoneGapではJavaの方でActivityの代わりにDroidGapを継承しているのだが、AndroidのGoogleMapはGoogleが提供しているMapActivityを継承して使うことで色々と隠蔽されるようになっているので。

クロスプラットフォームになるのかというと、そうでもなさそうな気がする。特にハードウェア依存の機能があるとどのOSも同じコードで動かせる、とはいかなくなるし、同じ種類のセンサーを持っていてもセンサーが返す値はOS/機種毎に違うんじゃないか?とかいう気がしている。
画面の解像度も機種毎に違うので、特にOSが違う場合にフォントサイズがどうなってしまうのかも気になる。

Androidのサービスを作る、暗黙的インテントを受信する、ホームスクリーンウィジェットを作るなど、PhoneGapのサポートしていない機能もある。が、これは無い部分をJavaで書けばなんとかなりそう。


■JavaからJavaScriptを呼び出す
最初に起動するactivityで、
super.loadUrl("file:///android_asset/www/index.html");
とやってhtmlを読み込んでいるんだけど、同じように
super.loadUrl("javascript://alert('kzsk')");
と、ブックマークレットを渡すと、JavaScriptの関数を呼び出して値を渡せる。phonegap.jsの中で使ってた。URLの文字数の制限がどれぐらいあるか調べてないけど、これでPhoneGapで機能的に足りない部分は自分でJavaのコード書いて補えるはず。

逆に、JavaScriptからJavaを呼び出すのはどうやるかはまだわからない。今調べてる。

0

Androidのギャラリーの「共有」メニューから呼び出せるアプリを作る

Androidのカメラで撮影した画像はギャラリーに保存される。そこで画像を選んで共有またはshareなどを選択すると、暗黙的Intent呼び出しで画像を別アプリに渡せる。
Flickrアップローダとか、EvernoteとかDropboxのアプリに写真を渡しているのはこのしくみを使っている。


まずManifest.xmlにintent-filterを追加する。
image/*でもいいが、それだと動画も渡せてしまうので手当たり次第mimeTypeを指定してみる。

<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/jpeg"/>
<data android:mimeType="image/jpg"/>
<data android:mimeType="image/png"/>
<data android:mimeType="image/bmp"/>
<data android:mimeType="image/bitmap"/>
</intent-filter>
ようするに、アプリに「私はimageをSENDするというアクションを受け取って適当に処理できますよ」と宣言させておくと、
他のアプリが「誰でもいいからSENDしたimageを受け取ってくれる人いませんか」と呼び出したときに連携できるようになる。


で、このintent-filterと関連付けられているクラス(普通なら一番最初に生成したjavaクラス)の
onCreate()内でintentを受け取る。
あらかじめ、受け取った画像を表示するためのImageViewを配置しておく。
private ImageView imageView;
this.imageView = (ImageView)this.findViewById(R.id.ImageView01);


暗黙的Intentで呼び出されたのか、普通のIntent呼び出しで起動したのか(つまりホーム画面からアプリのアイコンを押して起動したのか)を判別して処理する。
呼び出し元の名前だけで判別してる
Uri imageUri = null;
try{
imageUri = Uri.parse(getIntent().getExtras().get("android.intent.extra.STREAM").toString());
}
catch(Exception e){
e.printStackTrace();
}
if (imageUri != null) {
Log.v("ImageIntent", "暗黙的intentから起動");
Bitmap bmp = null;
try {
bmp = Media.getBitmap(getContentResolver(), imageUri);
}
catch (FileNotFoundException e) {
e.printStackTrace();
}
catch (IOException e) {
e.printStackTrace();
}
if (bmp != null) {
this.imageView.setImageBitmap(bmp);
}
}
else{
Log.v("ImageIntent", "普通に起動");
}
imageUriにはcontent://media〜〜というパスが入る。これはAndroid内のファイルシステムのパスではないが、Media.getBitmapに渡すとデータを読み出せる。
とりあえずImageViewに表示だけしておく。


あとEclipseだと日本語が化けるのでterminalからadb logcatしてる。


Bitmapを受け取ったら、後は適当に処理すれば良い。
リサイズとか
public Bitmap resize(Bitmap bmp, float scale){
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
return Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), matrix, true);
}



form/multipart-dataでgyazo.comにアップロードするとか、色々できる。
Manifest.xmlにpermissionを追加
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
public void upload(Bitmap bmp) throws ParseException, IOException{
File dataDir = new File(Environment.getExternalStorageDirectory(), this.getPackageName());
dataDir.mkdirs();
FileOutputStream tmp = new FileOutputStream(new File(dataDir, "tmp.png"));
bmp.compress(Bitmap.CompressFormat.PNG, 80, tmp);
tmp.close();

HttpClient httpClient = new DefaultHttpClient();
HttpPost post = new HttpPost("http://gyazo.com/upload.cgi");
MultipartEntity entity = new MultipartEntity();

entity.addPart("id", new StringBody("your-gyazo-id"));
entity.addPart("imagedata", new FileBody(new File(dataDir, "tmp.png"), "image/png"));
post.setEntity(entity);
post.setHeader("User-Agent", "TestAndroidApp/0.1");

httpClient.execute(post);
}
httpでform/multipart-dataでアップロードするには
の2つが必要。


■参考
このへんが参考になった

1

Android – アプリの設定画面を作る

画面遷移するアプリを作ったこと無かったのでやってみた。
AndroidではIntentで別のActivityを呼び出して画面遷移する。遷移ついでにIntentオブジェクトに値を入れて渡したり、返り値を受け取ったりする。

作った物はgithubに置いた



■動作
名前設定ボタンを押したらIntentで別のActivityに遷移して、そこで名前を設定する。保存(終了)すると前のActivityに戻る。もう一度やると、設定ActivityのEditTextの中身がさっき保存した値になっている。
device device2 device3



■Activityを追加する
  1. android.app.Activityを継承した新しいJavaクラスを追加する。ConfigActivity.javaにした。
  2. res/layout/ の中にレイアウト用XMLファイルを作る。既にあるmain.xmlをコピー&リネームでいい。config.xmlにして中身を適当に編集
  3. レイアウトを.javaに読み込む。config.xmlを作った時点でresourceが更新されているので、onCreate関数の中で setContentView(R.layout.config); すればいい。
  4. AndroidManifest.xmlを編集、<application></application>の中に新しいActivity名を書く
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.shokai"
          android:versionCode="1"
          android:versionName="1.0">
        <application android:icon="@drawable/icon" android:label="@string/app_name">
            <activity android:name=".MainActivity"
                      android:label="@string/app_name">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <activity android:name=".ConfigActivity" android:label="@string/app_name">
            </activity>

        </application>
        <uses-sdk android:minSdkVersion="7" />

    </manifest> 
    名前は .java ファイルの名前に合わせる。
これでConfigActivityを呼び出す準備ができた。



■ソースコード
返り値が必要な場合はstartActivityForResultで呼び出す。第二引数の数値はrequest codeで、onActivityResultで返り値を受け取る時に同じ値が戻ってくるので本物かどうかチェックするのに使う。
request codeは毎回違う値を生成した方がいいのかもしれない
MainActivity.java
package org.shokai;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.*;

public class MainActivity extends Activity implements OnClickListener{
    
    private Button buttonOpen;
    private Logger logger;
    private TextView textViewName;
    private String name;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        logger = new Logger(this.getResources().getString(R.string.app_name));
        textViewName = (TextView)findViewById(R.id.TextViewName);
        buttonOpen = (Button)findViewById(R.id.ButtonOpen);
        buttonOpen.setOnClickListener(this);
    }

    public void onClick(View arg0) {
        switch(arg0.getId()){
        case R.id.ButtonOpen:
            logger.v("click : ButtonOpen");
            Intent it = new Intent(this, ConfigActivity.class);
            if(name != null) it.putExtra("name", name);
            this.startActivityForResult(it, 1234);
            break;
        }
    }
    
    protected void onActivityResult(int reqCode, int resCode, Intent it) {
        logger.v("responseCode:"+resCode+", requestCode:"+reqCode);
        switch(reqCode){
        case 1234:
            setName(it.getStringExtra("name"));
            break;
        }
    }
    
    private void setName(String name){
        this.name = name;
        this.textViewName.setText(name);
    }
}


onCreate関数内でgetIntent()すると、MainActivityから呼び出したIntentオブジェクトが手に入る。この中から値を取り出す。
Intent.getStringExtra(key) も Intent.getExtras().getString(key) も同じ意味の関数だが、getExtras().getString(key) に存在しないkeyを与えると強制終了するので getStringExtra(key) の方を使った。こちらはnullが返ってくる。
finish()するとMainActivityに戻るが、戻る時にsetResultしておいて値を戻す。setResultは終了コード的な意味で、MainActivityのonActivityResultの第二引数に入る。
ConfigActivity.java
package org.shokai;

import android.app.Activity;
import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;
import android.os.Bundle;
import android.widget.*;

public class ConfigActivity extends Activity implements OnClickListener{

    private EditText editTextName;
    private Button buttonSave;
    private Logger logger;
    
    @Override
    public void onCreate(Bundle bundle){
        super.onCreate(bundle);
        logger = new Logger(this.getResources().getString(R.string.app_name));
        setContentView(R.layout.config);
        editTextName = (EditText)findViewById(R.id.EditTextName);
        buttonSave = (Button)findViewById(R.id.ButtonSave);
        buttonSave.setOnClickListener(this);
        Intent it = getIntent();
        String name = it.getStringExtra("name");
        if(name != null) editTextName.setText(name);
    }

    public void onClick(View v) {
        switch(v.getId()){
        case R.id.ButtonSave:
            logger.v("click : ButtonSave");
            Intent it = new Intent();
            it.putExtra("name", this.editTextName.getText().toString());
            this.setResult(0, it);
            this.finish();
            break;
        }
    }

}


■参考