カーネル/VM Advent Calendar 2011

この記事はKernel/VM探検隊のAdvent Calendar向けの記事です。

去年は別のブログで書いたんだけど、今年はAndroidネタなのでこっちで書きます。
KernelともVMともあまり関係無い気がするけど、AndroidはDalvikで動いているのでVMってことで(^^)

Android 3.1から Android Open AccessoryというUSBケーブルを使ってAndroidとArduinoボードを接続するAPIが搭載された。
PC並の処理能力を持つAndroid携帯をUSB Hostにして、ArduinoをUSB Clientにするのが当然だと思うんだが
Android携帯をClientにしてArduino側をホストにするという、逆転の発想でAndroid 2.3.4へのバックポートが実現している。
Android端末でUSBホストを実現するとなると、カーネルレベルでOTGに対応しなければならず結構面倒なんだが、Clientで動作するんだからそういう手間も要らない。

さて、これで何ができるかというと、ArduinoでできることをほとんどすべてAndroid側から制御することができる。
ArduinoではAVRやPICをつかって、PWM制御やDigital入出力、Analog入力などが行える。
つまり、Arduinoにサーボモータをつけて動かしたり、いろんなセンサをつけて、値を取り出すことができる。

例として使われているArduino MEGA 2560あたりを使うとPWMで12系統使えるので、かなりの数のサーボを制御することができる。
(電力の供給は別途考えないといけないけど)
つまりArduinoのボードとAndroid端末を組み込んだロボットなんかも作れたりする。
Android端末であれば、3GやWifiが使えるので、遠隔操作だってできてしまう。

ということで、極簡単にプログラムの構造を説明する。

Arduino側
Arduinoの場合は、CPUがAVRであるArduino純正の環境とMicrochipが作成したPIC24で動作する環境がある
それぞれ別のソースがあるのだが、AVR版の方が見やすいのでそちらの方をベースに話を進める。

AVR版のソースはArduino言語と呼ばれるC++のサブセットにArduino拡張を加えた言語を使用する。
といっても文法はCにクラスが追加された程度でなのでCだと思って読めばそれほど苦労はしない。
Arduinoのプログラムはスケッチと呼ばれ、setupとloopという関数が必須で、起動すると一度だけsetupが実行され、その後継続的にloopが呼ばれるという形式になる。
呼び出し処理はファームウェアで行われるので、特別なことは考えなくても良い様になっている。
割り込み処理も登録できるが、それほどクリティカルでない処理ならloopの中でポーリングする方が楽に処理できる。
当然だが、他の処理を止めない為にloopで実行する処理は最小限にとどめ、時間のかかる処理は分割して行う等の工夫は必要となるが、今回の場合Arduino側で複雑な処理をする必要はないので気にしない。
loopで行うべき処理は、
・USBの入力をみて、コマンドが来ていれば処理する。
・ボタンやセンサーをチェックして、値が変わっていたら、USBに出力する。
という極単純なものになる。

Android側
Android側では、Arduinoボードが接続されたら自動的にアプリの起動が行われる。
起動されたアプリケーションでは、接続されているデバイスを確認し、入出力ストリームを開き、ストリームへの入力を監視するだけの処理になる。
ストリームへの入力を検出したらコマンドを解釈して画面を更新し、画面操作を認識したら出力ストリームにコマンドを送る。
というArduino側の処理と対応した処理を行うだけで十分。

実際、RT-ADK&ADSというArduinoボードを使用して、Android 2.3.4または3.1以降のAndroid携帯を接続することで、たとえば、ジョイスティックを使って地図をスクロールしたりする程度のアプリなら簡単に作れる。
ということで、最後にAndroid側のソースを掲載して、終わりにしたい。
Arduino側はGoogle提供のdemokitのソースをそのまま使用する。

というわけで、簡単に作れて、結構面白いことができそうなので、どなたか何か作りませんか?
package com.example.adkmap;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
// import android.widget.TextView;

import com.android.future.usb.UsbAccessory;
import com.android.future.usb.UsbManager;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;

public class ADKMapActivity extends MapActivity implements Runnable {
private static final byte COMMAND_JOYSTICK = 6;
private static final byte COMMAND_BUTTON = 1;
protected static final int MESSAGE_JOY = 1;
private static final int MESSAGE_BUTTON = 2;
private static final int BUTTON1 = 0;
private static final int BUTTON3 = 2;
private static final int ON = 1;
private MapView mMapView;
private UsbManager mUsbManager;
private UsbAccessory mAccessory;
private ParcelFileDescriptor mFileDescriptor;
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;
// private TextView tv;
private static final String ACTION_USB_PERMISSION = “com.example.adkmap.action.USB_PERMISSION”;
private boolean mPermissionRequestPending=false;
private PendingIntent mPermissionIntent;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// tv = (TextView)findViewById(R.id.textView1);
// マップにズームコントローラを表示されるためにMapViewを取得する
mMapView = (MapView)findViewById(R.id.mapview);
// ズームコントローラを表示する
mMapView.setBuiltInZoomControls(true);
// USBアクセサリの許諾をとるためのrequestPermissionで使用するPendingIntent
mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(
ACTION_USB_PERMISSION), 0);
// USBアクセサリを取り外したときのインテントを受け取るためのフィルター
IntentFilter filter = new IntentFilter(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
// permissionRequestの結果を受け取るためのフィルター
filter.addAction(ACTION_USB_PERMISSION);
registerReceiver(mUsbReceiver, filter);
}
@Override
protected void onPause() {
super.onPause();
// tv.setText(“onPause”);
// onPauseではアクセサリをクローズする
closeAccessory();
}
@Override
protected void onResume() {
super.onResume();
// tv.setText(“onResume”);
// UsbManagerクラスのインスタンスを取得
mUsbManager = UsbManager.getInstance(this);
UsbAccessory accessory = null;
// 現在接続されているアクセサリのリストを取得する
UsbAccessory[] accessories = mUsbManager.getAccessoryList();
if(accessories != null && accessories.length == 1){
// アクセサリが一つだけ接続されていることを確認する。
accessory = accessories[0];
if (mUsbManager.hasPermission(accessory)) {
// 既にアクセサリの使用許可を持っていれば openAccessoryを実行する
openAccessory(accessory);
} else {
// アクセサリの使用許可を持っていなければrequestPermissionメソッドを実行する
synchronized (mUsbReceiver) {
if (!mPermissionRequestPending) {
mUsbManager.requestPermission(accessory,
mPermissionIntent);
mPermissionRequestPending = true;
}
}
}
}
}
@Override
protected boolean isRouteDisplayed() {
return false;
}
private void openAccessory(UsbAccessory accessory) {
// ファイルディスクリプタの取得
mFileDescriptor = mUsbManager.openAccessory(accessory);
if (mFileDescriptor != null) {
// mAccessoryに保存
mAccessory = accessory;
FileDescriptor fd = mFileDescriptor.getFileDescriptor();
// InputStream/OutputStremのオープン
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
// 通信スレッドの起動
Thread thread = new Thread(this);
thread.start();
} else {
}
}
@Override
public void run() {
int len = 0; // 読み出したデータのサイズ
byte[] buffer = new byte[16384]; // バッファ
int cur; // 現在処理しているデータの位置

while (len >= 0) {
try {
len = mInputStream.read(buffer);// InputStreamからデータを読み出し
} catch (IOException e) {
break;
}

cur = 0;
while (cur < len) {
int remain = len – cur;
switch (buffer[cur]) {
case COMMAND_JOYSTICK:
// コマンド番号がJOYSTICKに一致した場合
if (remain >= 3) {
// 残りのバイト数が3バイト以上あればメッセージを送信する
Message m = Message.obtain(mHandler, MESSAGE_JOY);
m.obj = new JoyMsg(buffer[cur + 1], buffer[cur + 2]);
mHandler.sendMessage(m);
}
// データ位置を3バイト進める
cur += 3;
break;
case COMMAND_BUTTON:
// コマンド番号がボタンに一致した場合
if(remain >= 3){
// 残りのバイト数が三バイト以上なら、ボタンのメッセージを送信する
Message m = Message.obtain(mHandler, MESSAGE_BUTTON);
m.obj = new ButtonMsg(buffer[cur+1],buffer[cur+2]);
mHandler.sendMessage(m);
}
cur += 3;
break;
default:
// JOYSTICKコマンド以外は無視して読み飛ばす
cur += 3;
break;
}
}
}
}
Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_JOY:
// ジョイスティックのメッセージだったら、handleJoyMessageを呼び出す
JoyMsg j = (JoyMsg) msg.obj;
handleJoyMessage(j);
break;
case MESSAGE_BUTTON:
// ボタンのメッセージだったら、handleButtonMessageを呼び出す
ButtonMsg b = (ButtonMsg)msg.obj;
handleButtonMessage(b);
break;

}
}
};

// ジョイスティックの情報を管理するクラス
protected class JoyMsg {
private int x;
private int y;

// コンストラクタ
public JoyMsg(int x, int y) {
this.x = x;
this.y = y;
}
// アクセサ
public int getX() {
return x;
}

public int getY() {
return y;
}
}
// ボタンの情報を管理するクラス
protected class ButtonMsg {
private int button;
private int state;
public ButtonMsg(int b, int s){
button = b;
state = s;
}
public int getButton(){
return button;
}
public int getState(){
return state;
}
}
protected void handleJoyMessage(JoyMsg j) {
int x = j.getX();
int y = j.getY();
// ジョイスティックは全く触っていない状態でもある程度の値を返すので±10以上動いた場合にのみスクロールを実行する。
if(x>10 || x < -10 || y > 10 || y < -10){
mMapView.getController().scrollBy(j.getX(), j.getY());
}
}
protected void handleButtonMessage(ButtonMsg b){
int button = b.getButton();
int state = b.getState();
switch(button){
case BUTTON1:
if(state == ON){
mMapView.getController().zoomOut();
}
break;
case BUTTON3:
if(state == ON){
mMapView.getController().zoomIn();
}
break;
}
}
// Broadcast Intentを受信するレシーバー
private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// intentからActionを取り出す
String action = intent.getAction();
if (ACTION_USB_PERMISSION.equals(action)) {
// permissionRequestの結果のIntentの場合
synchronized (this) {
// インテントからアクセサリを取り出す
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (intent.getBooleanExtra(
UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
// 使用が許可されていれば、openAccessoryを実行
openAccessory(accessory);
} else {
// 使用が許可されていなければ何もしない
// tv.setText(“Permission Denied”);
}
mPermissionRequestPending = false;
}
}else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
// ActionがACTION_USB_ACCESSORY_DETACHEDだった場合
// intentからAccessoryを取得する
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (accessory != null && accessory.equals(mAccessory)) {
// 現在使用しているアクセサリに一致する場合はcloseAccessoryを呼び出す
closeAccessory();
}
}
}
};
// アクセサリとの接続をクローズする
private void closeAccessory() {
try {
if (mFileDescriptor != null) {
mFileDescriptor.close();
}
} catch (IOException e) {
} finally {
mFileDescriptor = null;
mAccessory = null;
}
}

}

カレログの中の人と話をした

このブログは本来アプリや書籍のことを書く場所なのですが、カレログチェッカーなるアプリを作り、その後もカレログにいろいろ突っ込みを入れていたところ、カレログの会社(有)マニュスクリプトの三浦社長より、一度会って話がしたいという申し出を受けましたのでその件を書きます。
元々はTwitterで書いていた話なので、この話もTwitterで書いた方がいいのでしょうが、140文字でバラバラに発言すると一部だけを取り上げて誤解されるようなことがあるかもしれません。それは私にとっても、三浦社長にとっても良いことでは有りませんので、こちらにまとめて書くことにしました。

●まず、お話をすることになった経緯から
Twitterの私の書き込みをご覧の方はご存じでしょうが、私はカレログについて色々と突っ込み記事を書いてきました。その中で、かなり挑発的なことも書いていましたので、いつかカレログの中の人から何らかのコンタクトがあるだろうと思っていました。
「そんな言いがかりはやめてくれ」とか「ユーザーでも無いのに文句を言うな」とか、ありがちな反応です。
しかし、実際には、DM(DirectMessage)で一度会って話をしたい旨のご連絡でした。
ご連絡の文面は丁寧だったのですが、このような状況でしたので、「体育館の裏に呼び出された」的な恐れを感じるのは自然だと思います。もちろん、何か明らかな害悪を及ぼすなんてことは無いと思いますが、用心に越したことは有りません。
そこで、この面談の件について公開すること、第三者の同席を認めること、場所はこちらで指定すること という条件の元であればお話しすることもやぶさかではないという回答をしました。(実際はもう少し細かいことを言いましたが、要点は上記のようなものです。)
かなり失礼な返答にもかかわらず、こちらの条件を認めていただいたので本日(2011/9/14)直接会ってお話をさせていただきました。

●面談の内容
前半は三浦社長から、カレログの開発の経緯や開発体制などのお話を伺いました。
その内容についてはセンシティブなものも含まれますのでここでは詳細は控えますが、特に目新しい情報があったわけではありません。
後半は私および同席した知人(こちらは氏名は伏せさせていただきますが、それなりにセキュリティなどにうるさい人物)から、カレログサービスについて指摘というか、ダメ出しをさせていただきました。
・アプリで規約を何度も読ませてもそれを読んでいるのは誰か不明
・端末所有者の同意を担保することはアプリだけでは不可能
・設計時点から契約者、端末所有者、カレログサービスという三者の存在がちゃんと定義されていない。
・位置情報サービス(GPS機能)を使うことすべてが悪みたいな風潮を巻き起こしかねない(Winny事件の結果P2Pがすべて悪者みたいな風潮が出てしまったのと同じ)
・カレログ管理画面を使う人間に対して規約への同意などを取っていない。
・管理画面のID/PWの管理がいい加減すぎる
・悪意の誰かがカレログを使ったとして、端末所有者がそれを見つけてもカレログ側では管理画面の使用者を特定する方法がない
・現在のサービスの品質をみると、サーバに保持している個人情報を十分に保護しているとは思えない
などなど、言いたい放題言わせていただきました。(他にもいくつか申し上げましたが、センシティブな内容であったりするので公開は控えます。)
総じて大変失礼な言い方だったと思います。三浦社長にはこの場を借りてお詫びします。

そして、三浦社長からは今後のサービスの改善について、協力をしてくれないかというオファーもいただいたのですが、さすがにそれはご勘弁いただきました。私としても興味が無い訳ではないのですが、中の人になってしまうと、表立って自由にものが言えなくなってしまいます。

●まとめ
「サービス開始時点では、これほどの反響が有るとは想像もしていなかった、よくても週刊誌などにちょっと取り上げられる程度が関の山だと思っていた」という三浦社長の言葉があったのですが、それがすべてではないかと思います。
位置情報というかなりセンシティブな情報を、あまりにカジュアルに扱いすぎたというのが今回のカレログの問題点だと思っています。
法的なことは私の専門外なので、意見は差し控えますが、技術的な面、モラル的な面で脇が甘かったというのが私の感想です。
すくなくとも三浦社長とお話をした範囲では、炎上まで予想してうまく立ち回ったというわけではないという印象は持ちました。(印象ですので正しいかどうかはわかりません)

●最後に
私のスタンスはこれまでと変わりません。
(有)マニュスクリプトとは利害関係、契約関係はありませんし、今後もこれまで同様に言いたいことを言います。
このことについては三浦社長にもご了承いただいています。(興味が失せたら何も言わなくなると思いますけど)

結局あまり面白い話にはならなかったので、この記事を書くのにえらく時間がかかってしまいました。(もっと三浦社長がぶっ飛んだ方だったら、おもしろおかしく書くこともできたのですが)

同席してくれた某氏ありがとうございました。他にもこの件で心配してくれた友人たちに感謝します。

カレログチェッカーの作り方

カレログチェッカーをつくるには

下記の説明だけではよく分からない方は、「基礎から学ぶAndroidアプリ開発」をどうぞ

0.AndroidSDKをインストール
1.Eclipseを起動する。
2.新規Androidアプリ作成(この時CreateActivityチェックはつけない)
3.新規クラス「KarelogReceiver」を作成(SuperクラスはBroadcastReceiver)
4.「KarelogReceiver」のonReceiveメソッドの作成
ACTION_POWER_CONNECTEDのインテントを受け取ったらアプリケーションを検索してノーティフィケーションを表示するだけです。
ACTION_DISMISS_NOTIFICATIONというのは、ノーティフィケーションをタッチされたときに発行するインテントで、ノーティフィケーションを消去するだけの処理を行います。

public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(action != null){
if(action.equals(Intent.ACTION_POWER_CONNECTED)){
String foundApp = checkPackages(context);
if(foundApp != null){
showNotification(context,foundApp);
}
}else if(action.equals(ACTION_DISMISS_NOTIFICATION)){
NotificationManager manager =
(NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(NOTIFICATION_ID);
}
}
}
5.checkPackagesの作成
パッケージリストを順番に捜してみる処理です。
先にアプリケーションのリストを取り出してから、検索した方が効率がいいですが、チェックする数が多くはないので、ここでは手を抜いています。

public static String checkPackages(Context context){
String foundApp = null;
for(int i=0;i<checkPackages.length;i++){
foundApp = findApplication(context,checkPackages[i]);
if(foundApp != null){
break;
}
}
return foundApp;
}
6.findApplicationの作成
引数にマッチするアプリケーションがインストールされていれば、パッケージ名を返します。
最初に見つかった一つだけを返すので、複数検出したい場合は、リターン値をListとかに変える必要があります。
引数のパッケージ名は先頭一致なので、ドメイン名の部分だけにしておくと同じベンダーのアプリがすべてヒットします。

public static String findApplication(Context context,String packageName){
PackageManager packageManager = context.getPackageManager();
List<ApplicationInfo> applicationInfoList =
packageManager.getInstalledApplications(0);
for(ApplicationInfo info : applicationInfoList){
if(info.packageName.length()>=packageName.length()){
if(info.packageName.substring(0, packageName.length()).equals(packageName)){
return info.packageName;
}
}
}
return null;
}
7.showNotificationの作成
ノーティフィケーションの表示を行います。書籍のCHAPTER05で説明しているのと同じ処理です。

public static void showNotification(Context context,String foundApp){
long time = Calendar.getInstance().getTimeInMillis();
String message = “Found:”+foundApp;
Notification notification=new Notification(R.drawable.icon,message,time);
Intent intent = new Intent(context,KarelogReceiver.class);
intent.setAction(ACTION_DISMISS_NOTIFICATION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
notification.setLatestEventInfo(context,context.getString(R.string.notify_title),context.getString(R.string.notify_message)+foundApp,pendingIntent);
NotificationManager manager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(NOTIFICATION_ID,notification);
}
8.各種定数など
各種定数の定義です。NOTIFICATION_IDは適当な値です。別に何でもいいです。
checkPackagesはArrayにしているので、複数のパッケージをチェックすることもできます。
先にあるとおりドメイン名だけにしておくと同じベンダーのすべてのアプリをチェックすることができます。

public static int NOTIFICATION_ID = 1020;
public static String[] checkPackages = {“jp.karelog.gpsmanager”};
public static final String ACTION_DISMISS_NOTIFICATION = “be.watana.karelogchecker.service.dismiss”;
9.Manifestの修正
receiverだけのアプリなのでActivityはありません。

<?xml version=”1.0″ encoding=”utf-8″?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
package=”be.watana.karelogchecker” android:versionCode=”1″
android:versionName=”1.0″>
<uses-sdk android:minSdkVersion=”8″ />
<application android:icon=”@drawable/icon” android:label=”@string/app_name”>
<receiver android:name=”.KarelogReceiver”>
<intent-filter>
<action android:name=”android.intent.action.ACTION_POWER_CONNECTED” />
<category android:name=”android.intent.category.DEFAULT” />
</intent-filter>
</receiver>
</application>
</manifest>
10.あとはImportするクラスを調整して 細かいエラーを直せばOKです。
stringsリソースが必要なので適当なものを追加する必要があります。
Eclipseのエラーを見れば何が必要かはわかるので、適当に作成します。
Layoutリソースなどは不要なのでlayoutフォルダーごと削除してしまっても問題有りません。
日本語と英語に対応するには、CHAPTER04の最後に有るとおりvalue-jaディレクトリを作って日本語用strings.xmlを作成します。

書籍に乗っていない情報はfindApplicationの中のPackageManagerを使う部分だけだと思います。
チェックするパッケージ名を変えるだけで簡単に「なんとかチェッカー」になります。

Eclipseプロジェクトのアーカイブは KarelogChecker.zip

ライセンスはApache License 2.0です。
このプログラムはAS IS(あるがまま)で提供されます。動作することは保証しません。
このコードを使ったこと、使わなかったことによる結果については一切の責任を負いません。
ソースコードについての質問には基本的に回答しません。
ソースには上記のコード以外に、テストのコードや定期実行、起動時実行などを試したものが含まれています。
使用する際にはかならずパッケージ名の変更をしてください。

KarelogChecker

カレログというサービスが始まったらしいのですが、なかなか香ばしいサービスのようです。
とりあえず、カレログアプリというものをカレのスマートフォンにインストールするらしいので、カレログアプリというアプリをインストールされているかどうかだけをチェックするアプリとして作成してみました。
ほとんど練習問題というべきアプリです。

電源オンの時にチェックしたり、一定間隔でチェックしたりすることを考えたのですが、電源オンの時に実行するにはパーミションの取得が必要ですし定期実行にはAlarmサービスの実行が必要です。

そこで、特別なパーミションが必要ない充電ケーブル接続イベントで起動するようにしました。
起動してもやることはアプリケーションリストを取得してカレログアプリを捜すだけです。
捜した結果見つかった場合はノーティフィケーションを表示します。

アプリのランチャーにはアイコンが表示されませんから、他の人には簡単には見つからないでしょう。

カレログのサービスに書いてある説明に従えば、「必ず端末の所有者の許可を取ってから入れること」と書いてあるので勝手に入れられることは無いので、このチェッカーが活躍することはあり得ないので問題はないはずですね。
もしこのアプリで知らない間にインストールされたカレログが発見された場合は、本来カレログの意図しない使われ方をしていることになりますから、そういった不正利用を排除することはカレログ側にとってもメリットであるはずです。

iOSアプリUIWebViewのスクロールを止める

UIWebViewを使ってインタフェースを作りたいとき、ユーザの操作でスクロールされたりするのは困るので、スクロールやバウンスを止めたい時がある。

要するにWebViewのSubViewの中のUIScrollViewのBounceとScrollを止めればいい。

for (id subview in webView.subviews){
if ([[subview class] isSubclassOfClass: [UIScrollView class]]){
((UIScrollView *)subview).bounces = NO;
((UIScrollView *)subview).scrollEnabled = NO;
}
}
これでいける

RebootLogger バージョンアップ

再起動のログの表示が、昇順固定だったので、最新の再起動時刻を見ようとするとスクロールしなければならないのが面倒だというコメントがあったので、昇順・降順を切り替えるボタンを追加しました。
確かに有る程度以上量が増えてくると最新の時刻が表示から溢れてしまうので、見にくくなりますから降順での表示も有るといいですね。

アプリ内のHTMLファイルをWebViewで表示する

本ではWebViewについてほとんど触れてませんが、Google Maps JavaScript API V3を使うコラムで、WebViewを使うと書いてあります。

本文ではサーバを用意してそこから読み出すように書いていますが、サーバを用意するのは面倒なので、リソースとしてHTMLファイルや画像ファイルを用意して読み出したいと思うのは当然でしょう。

そういう場合は、WebViewクラスのloadUrlメソッドにURLとして “file:///android_asset/filename.html” の様に指定することでプロジェクトの中でassetsという名前のフォルダのファイルを参照することができます。

フォルダ名がassetsで参照がandroid_assetとsが無いのは、多分間違いが残ってしまったのでしょう、名前が異なるのが正しいので注意が必要です。

さて、問題はこのassetsフォルダーでは言語などを勝手には識別してくれないことです。

Android 2.2(API Level 8)以上であれば、file:///android_res/raw/filename.htmlという参照が可能で、/res/raw/フォルダなどにHTMLファイルを置くことで、言語やその他のリソース切り替え機構が動きます。

API Level5くらいから使えるのであれば、1.6以下を無視するという手がありますが、さすがにLevel7以下を無視するとXperiaまでが対象外になってしまいます。

そこで、先ほどのassetsフォルダを使う為にプログラム側で言語を切り替えて対応することを考えます。

日本語かどうかでページを分けたい場合、以下のようにすればよい様です。(HT-03A,IS01では旨く動きました)

WebView wv = (WebView)findViewById(R.id.webview);
if(Locale.getDefault().equals(Locale.JAPAN)){
wv.loadUrl(“file:///android_asset/ja-index.html”);
}else{
wv.loadUrl(“file:///android_asset/index.html”);
}
それぞれのファイルではリンク先はそれぞれの言語に応じたファイルにリンクすることになります。
HTMLファイルの中での参照ではすべてフラットな同じディレクトリ内に存在するので、ファイル名だけで参照可能です。逆に通常のページの様にディレクトリで階層を作ることはできません。

「基礎から学ぶAndroidアプリ開発」およびアプリのブログ

「基礎から学ぶAndroidアプリ開発」および私の開発しているアプリの為のブログを開設しました。

今のところ記事はないですが、記事の間違いが見つかったら何か書くかもしれません。

質問が来たら返事を書くかもしれません。

あまり期待しないでください。

本当はこのブログに何も書かずに済むのが一番いいのです。