Android 4でPicker(Titanium.UI.PICKER_TYPE_TIME)

またまたAndroid BK通信です。

Titanium Mobileは正式にはAndroid 4に対応していません。しかし、Androidもさすがにアホじゃないので下位互換性はほぼ保たれているため、ほとんどの場合は問題なく動作します。

という書き方をすればお気付きかもしれませんが、当然動かない場合もあります。今日はそのような問題のひとつ、Pickerについて報告します。

Android 4からTimerPicker.javaupdateInputStateという関数が追加されています。Titanium MobileでcreatePickerした際にtypeプロパティをTitanium.UI.PICKER_TYPE_TIMEにすると、このTimerPickerが呼び出されるのですが、実はこのupdateInputState関数には問題があって、IME経由でPickerの値を変更した場合はいいのですが、+ボタンや−ボタンで変更すると入力値のチェックが甘くNullPointerExceptionでクラッシュしてしまいます。報告が上がってはいますが、修正はされていません。

var win = Ti.UI.createWindow();
var picker = Ti.UI.createPicker({type:Titanium.UI.PICKER_TYPE_TIME});
win.add(picker);
win.open();

これだけのコードで再現しますが、Android 3以下では動作しても4からはボタンで値を更新しようとするとクラッシュします。数字をタップしてIME経由で数字を入力すると落ちません。

というわけで、Android 4に対応する必要がある方はTitanium.UI.PICKER_TYPE_TIMEは諦めて

var win = Ti.UI.createWindow();
var hours = Ti.UI.createPickerColumn({width:Titanium.Platform.displayCaps.platformWidth/2});
for(var i = 0; i <= 24; i++){
  var row = Ti.UI.createPickerRow({title:i.toString() + '時'});
  hours.addRow(row);
}
var mins = Ti.UI.createPickerColumn({width:Titanium.Platform.displayCaps.platformWidth/2});
for(var i = 0; i <= 59; i++){
  var row = Ti.UI.createPickerRow({title:i.toString() + '分'});
  mins.addRow(row);
}
var picker = Ti.UI.createPicker({width:Titanium.Platform.displayCaps.platformWidth, useSpinner:true, columns:[hours, mins]});
win.add(picker);
win.open();

とかで逃げるしかありません。

Popularity: 1% [?]

自宅サーバが亡くなりました

2009年の9月から稼働していたhpのProliant ML115 G5が(おそらく電源の故障がきっかけで)動かなくなってしまいました。マザーボードの交換がめんどい機種なので部品の交換では難しく、思い切って近所のPC Depoに出かけたらセール品だけでなんとかなりそうだったので、時間もないので購入してしまいました。さようならAMD、こんにちはIntel。量販店にXeonなんかないですからCore i5にしましたが、対応マザーボードも安くてECC無しのメモリで動くからまあいいです。それに、よく考えたらSandy Bridgeは家のどのマシンよりハイスペックです。通販で買う方が多少は安いパーツもありましたが、DDR3のメモリとマザーボード、ケースでしめて35,000円くらいでした。HDDとDVDドライヴ、ケーブル類は元のマシンからの流用です。

ついでにESXi 5.0に更新しました。バージョン3の時代みたいに無茶なことをしなくてもHDDにインストールできるようになったんですね。

ただ、いくつかデータプールにあったVMが消えてる気がするんだよな…

Popularity: 1% [?]

Titanium MobileでLocal Notificationを実装する

iOSにはリモートからメッセージなどを通知するPush Notificationとインストール済みアプリから通知するLocal Notificationという二つの機構があります。以前、Push Notificationのサンプルはgistに公開したことがあるので、今回はLocal Notificationについて書いてみます。

Local Notificationが出来るのは

・ポップアップまたはステータスバーへの通知
・通知の際のサウンドの再生
・バッヂの操作(アプリのアイコンの数字のことね)

です。通知が発生した際にステータスバーまたはポップアップのOKをタッチしてアプリケーションを起動することができます。端末がスリープ状態のときはロック解除のところが下のalertActionで指定する文字になります。通知がユーザに無視された、またはユーザがキャンセルボタンをクリックした場合は何も起きません。また設定で通知をオフにした場合は何も表示されません。

*アプリを企画する方はここに注意してください。アプリを通知から必ず起動する方法はありません(やったらリジェクトされます)。

それから、通知を予約したアプリがフォアグランドで実行中には通知は実行されません。ただし、通知が発生していたことはアプリに通知されます。後ほど詳しくみていきます。

サウンドの再生ですが、どうやらシステムに組み込まれたサウンドを鳴らす方法はありません。サウンドデータを自前で用意することになります。対応しているフォーマットはcaf、aiff、mp3まで動作確認できました。wavとかもいけるかもしれませんが試していません。

サウンドの再生は、仕様では30秒までとなっていますが、29秒でないと動作しませんでした。またボリュームはプログラム側から制御することができません。端末でミュートしていたら音は出ません(じゃなきゃ困ります)。

繰り返しは毎日(daily)、毎週(weekly)、毎月(monthly)、毎年(yearly)から選択できます。何も指定しなければ一回だけ実行されます。繰り返しの場合は開始日時に過去の日付を指定すると次回からの実行になりますが、繰り返しがない場合は即時実行になってしまいますのでプログラム作成時に注意しましょう。

以上を踏まえて、作りたいアプリがLocal Notificationを使うのに相応しいかどうかを判断しましょう。

さて、実装です。Local Notificationではアプリいつ、どんなことをするかをiOS側のAPIに予約して、OKボタンの押下などの結果を受け取ります。

Ti.App.iOS.cancelAllLocalNotifications();
var notifications = [];
notification_params = {
      alertBody: 'こんにちは、こんにちは',
      alertAction: 'OK',
      userInfo: {
        data: {param1:'これはparam1', param2:'これはparam2'}
      },
      sound: 'sound.mp3',
      repeat: 'daily',
      date: new Date((new Date()).getTime() +(1000 * 10))
    };
notifications.push(Ti.App.iOS.scheduleLocalNotification(notification_params));

まずcancelAllLocalNotificationsを実行して過去にこのアプリから登録したLocal Notificationを全てキャンセルにします。必要のないアプリであればここで実行する必要はありませんが、個別に通知を選んでキャンセルすることができないので、こうして一気にやってしまいます。

notificationsという配列を使っていますが、後で利用することはありません。が、アプリの終了後にすぐに消えてしまうと困るのでこうしていちいち宣言しています。

これでdateパラメータで指定された時刻(この場合は10秒後)に通知が実行されます。急いでアプリを閉じて待ちましょう。

実行されましたか?

あ、いまはまだこの記事を読んでいるだけで、実際に手を動かしてはいないんですね。わかりました。でも折角だからやってみましょう。今日は雨だし付き合いますよ。

実行されましたか?

よかったですね。ではもっと細かいところを見てみましょう。Local Notificationの予約はボタンを押せば実行されるようにしますか?でも、ボタンを押した瞬間にアプリを強制的に終了した場合はどうなるんでしょうか。変なタイミングで電話がかかってきてしまったら?Ti.App.iOS.scheduleLocalNotificationはvoid型で戻り値がないので、なんだか落ち着かない感じがしませんか?そうなんです、通常はLocal Notificationの予約はバックグランドで実行するのがベストプラクティスらしいので、ちょっと変更しましょう。値の受け渡しのデモもやります。

Ti.App.Properties.setString('message', 'こんにちは、こんにちは!');
Ti.App.Properties.setString('when','2012/04/23 00:15:00');
var service = Ti.App.iOS.registerBackgroundService({url:'service.js'});

service.jsはこちら:

if(Ti.App.Properties.hasProperty('when')){
	Ti.App.iOS.cancelAllLocalNotifications();
	var notifications = [];
	notification_params = {
	      alertBody: Ti.App.Properties.getString('message'),
	      alertAction: 'OK',
	      userInfo: {
	        alertMessage:'ピンポンパンポン!'
	      },
	      sound: 'sound.mp3',
	      repeat: 'daily',
	      date: new Date(Ti.App.Properties.getString('when'))
	    };
	notifications.push(Ti.App.iOS.scheduleLocalNotification(notification_params));
}
Ti.App.currentService.stop();

アプリがフォアグランドにある場合はLocal Notificationは実行されないといいましたが、実行するはずの時間になるとその旨だけがアプリに通知されます。これはnotificationというイベントとして捕まえることができます。userInfoを使って値の受け渡しまで実行できます。

Ti.App.iOS.addEventListener('notification', function(e) {
  var dialog = Ti.UI.createAlertDialog({
  	title:'お知らせ',
  	message:e.userInfo.alertMessage,
  	buttonNames:['OK', 'お黙り'],
  	cancel:1
  	});
  dialog.show();
});

こんなもんでしょうかね。Local Notificationのパラメータにはいくつか面白いものもあるのでぜひマニュアルをご覧ください。

Popularity: 2% [?]

また日本語のアプリ名に出来なくなってる?

AndroidManifest.xmlをplatform/android以下に置いて、アプリ名を日本語にしてビルドしたら動かなくなっていました。何の影響ですかね?

/Library/Application\ Support/Titanium/mobilesdk/osx/1.8.2/android/builder.pyをいじったらいけたんで、とりあえずdiff晒しておきますね。。。

1057,1058c1057,1058
<	manifest_source = manifest_source.replace(ti_activities,"\n\n\t\t".join(activities))
<	manifest_source = manifest_source.replace(ti_services,"\n\n\t\t".join(services))
---
>	manifest_source = manifest_source.replace(ti_activities,"\n\n\t\t".join(activities).encode("utf-8"))
>	manifest_source = manifest_source.replace(ti_services,"\n\n\t\t".join(services).encode("utf-8"))

* 追記

2.0.1GA2でも同じですね。1083行目あたりに同じのがあります。

Popularity: 1% [?]

TitaniumでAndroidのServiceを

マニュアル通りにやっても動かなかったので、メモしておきます。

AndroidのServiceについては別の資料をご覧頂くとして(常駐アプリみたいなものです)、その起動や停止をTitanium Mobileから制御する方法について説明します。1.8.2で動作確認しています。

Serviceを利用するにはJSに記述する以外にも、tieapp.xmlに追記する必要があります。

<android xmlns:android="http://schemas.android.com/apk/res/android">
        <services>
            <service type="interval" url="myservice.js"/>
        </services>
</android>

これは、myservice.jsというファイルに記述されたServiceを指定された間隔(interval)で起動する、という意味になります。tieapp.xmlにこれを指定すると、build/android/gen/APP_ID/MyServiceService.javaというファイルが生成されます。中身はTiJSIntervalServiceを継承したMyserviceServiceクラスの実装で、build/android/AndroidManifest.xmlに

<service  android:name="APP_ID.MyserviceService" />

が追記されていれば成功です。APP_IDのところは実際には先ほどの生成されたJavaのファイルにも記述されているパッケージ名になります。

ところが、いろいろと作業する内にtieapp.xmlを編集するだけでは上のようにはならず、

<service  android:name=".MyserviceService" />

のようになったことがありました。困ったことに、一度こうなるとそのまま変更されなくなってしまうようです。これでは当然動作しないので、一度ビルドしてbuild/android/AndroidManifest.xmlが作成されたら、プロジェクトディレクトリ直下にplatform/androidディレクトリを作成し、AndroidManifest.xmlをコピーします。

$ mkdir -p PROJECT_HOME/platform/android
$ cp PROJECT_HOME/build/android/AndroidManifest.xml PROJECT_HOME/platform/android/

platform/android/AndroidManifest.xmlに記述された内容はtieapp.xmlの情報とマージされて、build/android/AndroidManifest.xml.genとなります。platform以下のXMLの方が優先されるみたいです。なので、これを編集して

<service  android:name="APP_ID.MyserviceService" />

という行を追加します(しつこいようですが、APP_IDはパッケージ名です)。

次に、myservice.jsを用意するわけですが、XMLの方には記述されていませんが設置するのはResources直下ではなく、Resources/android以下になります。もし起動時にデータを受け渡しする場合はintentにputExtraで指定することができます。Serviceは即時実行されますが、intervalを指定すると二度目以降は指定されたintervalの間隔(ミリ秒)で実行されます。

// app.js
var activity = Titanium.Android.currentActivity;
var service_intent = Ti.Android.createServiceIntent({
  url: 'myservice.js'
});
service_intent.putExtra('interval', 5 * 60 * 1000); //5分後に実行
service_intent.putExtra('my_data', 'this is my data');
service = Ti.Android.createService(service_intent);
service.start();
// android/myservice.js

var service = Ti.Android.currentService;
var service_intent = service.getIntent();
if(!Ti.App.Properties.hasProperty('notificationCount')){
  Ti.App.Properties.setInt('notificationCount', 0);//初回起動時は無視
}else{
  //5分後にresumeしたときの動作
  Ti.App.Properties.removeProperty('notificationCount');
  var my_data = service_intent.getStringExtra('my_data');
  Ti.API.info(my_data);//データの引き渡しができた
  service.stop();
}

これでログに文字列を出力するだけのServiceを作ることができました!最高のアプリです。バカ売れ間違い無しですね。

Serviceの活躍する場面ですが、例えばここから起動中のアプリにfireEventで何か実行させたりすることができます。setIntervalとかsetTimeoutで動作を制御しようとすると、Androidの場合は画面スリープの制御が出来なかったりしていまいち信頼性が低いので、Serviceを起動してそこからコントロールするのがいいと思います。あるいは、バックグランドで実行する位置情報の更新にも便利ですね。

そうそう、iOSのlocal notificationの変わりにもなりそうです。

また、常駐しているサービスから何かのきっかけでAlarm Managerを呼び出して停止中のアプリを起動させたりすることができるともっと面白いことができるかもしれません。が、Titanium MobileにはAndroidのAlarm Managerを扱う仕組みがないので、これは別途モジュールで作成する必要があります。

続く

* update

ところが、Titanium製アプリでバックグランドでServiceを実行すると、Androidでホームボタンを押してアプリを閉じた場合は問題ないのですが、backボタンで閉じた場合はServiceがresumeしなくなることが判明しました。

そこで、backボタンを押したときにホームボタンと同じ動作をするよう変更してみたところ、Serviceは無事動き続けました。

  win.addEventListener('android:back', function(){
    var intent = Ti.Android.createIntent({
      action: Ti.Android.ACTION_MAIN
    });
    intent.addCategory(Ti.Android.CATEGORY_HOME);
    Ti.Android.currentActivity.startActivity(intent);
  });

backボタンでアプリが閉じてしまうrootウィンドウにこんなイベントリスナを追加するとうまくいきました。

Popularity: 1% [?]

Android開発でハマッたところを徒然なるままに

imageプロパティのパスがおかしい

imageViewとかのimageプロパティはiOSと違って「/path/to/image.png」みたいに先頭をスラッシュにしてあげないと何も表示されない。ただしbackgroundImageは「path/to/image.png」で通じる。

難易度:1

Android 4には対応していない

ドキュメントにあるように、Titanium Mobileは1.8.2時点ではまだAndroid 4には対応していない。意味不明のエラーになることがある。例えばcreatePickerでtypeプロパティにTitanium.UI.PICKER_TYPE_TIMEを指定すると落ちる。

難易度:2

ActivityIndicatorの挙動が違う

iOSでは、ActivityIndicatorはそれをaddしたviewやwindowオブジェクトが消えれば画面から消えるが、Androidの場合はhide()を呼び出してやらないと表示されたままになる。

難易度:2

IS03ではdialogはwindowにaddしないといけない

他の機種で動作していたとしても、IS03ではcreateDialogしたdialogオブジェクトが表示されないことがある。この場合、前面にあるwindowオブジェクトにaddすると表示される。

難易度:3

tagGroupを使うとアプリが終了しない

Ti.Android.currentActivity.finish();をコールしてもアプリが終了しない。tabGroup.close();を呼び出すとズドンと終了する。

もっとも、AndroidアプリはActivityの停止を呼び出しても、それは停止してもいいとフラグを立てるようなものなので、必ずしも停止するとは限らない(けど実質的には停止する…)ので要注意。

難易度:4

window.open();で別スレッドになることがある?

createWindowしたオブジェクトにイベントリスナを追加して、その前に開いていた画面を裏で閉じたり変更したりするとき、ぬるぽで落ちることがあった。どうやらtab.open(win);ではなくwin.open();だと別スレッドになることがあるらしい。ちなみにコードは正しくiOSでは必ず動作する。

難易度:4

imageViewの画像が更新されない

一度画面に表示したimageViewのプロパティを外部からいじっても更新されないことがある。image_view.image = ‘/path/to/new_image.png’;のように記述しても必ずしも更新してくれるわけではない。こちらもコードは正しくiOSでは必ず動作する。同じコードでAndroid対応するとimageView自体を消して作り直すなど根本的な変更が必要になる。

詳細を調べきれていないので申し訳ない。

難易度:5

Distribute用ビルドのJSファイルのコンパイル機能がおかしい

Distribute用ビルドでは「var object = {};」がコンパイルエラーになる。「var object = new Object();」なら通る。

難易度:5+

Popularity: 2% [?]

AndroidのDistribute用ビルドでコンパイルエラー

Titanium MobileでAndroidアプリを作成し、実機テストも終わってさあいよいよだとDistribute用ビルドを作成したときのこと。1.8にはJSファイルを読めないようにコンパイルする処理があります。Distribute用ビルド以外はこのコンパイル作業は実行されないのですが、実機での動作確認まで終わった開発用のビルドでは問題なかったソースコードがこのコンパイル時にエラーでビルドに失敗するではありませんか。いくつかのエラーは、例えば同じプロパティが複数回同時に宣言されているだとか、エラーメッセージを読めばわかるものだったのですが、最後に残ったエラーはただの例外でしかないので何が起きているのかさっぱりわかりません。

//例えばこんなエラー
[ERROR] unrecognized error encountered: " % se

そこで、コンパイルが止まるJSファイルをいったん空のファイルにすると、今度はビルドは通りました。なるほど、ビルドスクリプトがこのJSファイルをうまく処理できないに違いありません。そこでえっちらおっちら手動バイナリサーチです。400行のJSファイルをまずは200行で区切って再度コンパイルします。といってもシンタックスエラーになってはダメなので、最低限文法だけは合っている(実行時エラーが起きても問題ありません)ファイルを作る必要がありますから210行とかちょっと半端になります。これでビルドが通れば消した200行の中に問題の部分があり、そうでなければ残っている200行に問題がある。後者だったので、今度はその200行を100行で切って文法的に正しくなるよう整形してまたコンパイル、今度は50行にして、と続けて四回目であれ?と気になって試してみたらビンゴでした。

いいですか?心を落ち着けて、よく見てください。これが、ビルドを阻んでいた問題のコードです:

var form_data = {};

…え?

驚かれましたか?そうなんです。これなんですよ。ぶっちゃけていえば、ビルドスクリプトの正規表現がおかしいとしか思えない(どうやらトークンにパースしてどうこうみたいな処理じゃなくて単純に正規表現で実装されてるっぽいんです)のですが、これを

var form_data = new Object();

にすれば直ります。ここで冒頭の画像に戻ります。

Popularity: 1% [?]

invoker.js

Titanium Mobile(Android)でSQLiteを使おうとして、invoker.js がどうしたってエラーが出てきたので調べたら、1.8系ではResouces以下に置いたdbファイルを指定するのに先頭に「/」が必要になっていた。本家のQ&Aにも見つかっている

Resouces/my_app.dbというファイルを用意しているなら

if Ti.Platform.osname == 'android'
  db = Ti.Database.install('/my_app.db', 'my_app')
else
  db = Ti.Database.install('my_app.db', 'my_app')

こんな感じ。

Popularity: 1% [?]

register_globals?そんなのデフォルトでオフですよ

Ruby on Railsのmass assignment絡みの脆弱性について、すごくよくまとまった記事。Railsで開発している人は必見ですね。

かつてPHPにはregister_globalsというこれと似た機能があり、現在はデフォルトでオフかつ5.4以降は廃止されたのですが、それまでは散々恐ろしい問題を引き起こしていました。PHPのウェブアプリケーションにセキュリティ問題が多いという評判が定着してしまったのは、これが主な原因といっても過言ではありません。PHPは何年もかけてようやく負の遺産を返済することができたのですが、Railsにはこのregister_globals相当の機能があり、今回見つかった脆弱性はかつてPHPが経験したことをそのままなぞっているようで興味深いところです。

ちなみに、Railsの本と名乗っていながら冒頭からRubyのオブジェクトの仕組みからメタプログラミングについて詳細に解説しまくる名著『実践Rails』では(初版なら)142ページ目にこの問題についての解説があります(Safari Onlineはこちら)。さすがです。

というわけで、何年も前に指摘されていた脆弱性に今頃慌てふためいている愚かでコンピュータサイエンスを学ぶには無能すぎてRailsくらいしか使えないくせにPHPerだなんだと他人の尻馬に乗って調子こいてたボケナス共は、土下座してmodelの修正に取りかかるがよい。わっはっは。

Popularity: 5% [?]

Ode to Ide

井出さん、おはようございます。小生、貴方様のことについては全く何も存じ上げませんが、なんか音楽を聴けとかいってリスト作られちゃってる人だってことは認識しております。

そこで、貴方様とは縁もゆかりもない私めも、ここは一丁やってみるべと、貴方様が聴くべき音楽レコードを10個挙げさせて頂く次第であります。

黙って聴け!

その1:Half Japanese: Band That Would Be King

いきなりVHSかよ!といふなかれ。誰がCDだっていった?

その2:The Chameleons: This Never Ending Now

基本だから。これ聴いてない輩が何を言っても信じない方がいいよ。

その3:なし

あのね。こんなリストとか作られて堂々と公開されちゃって、今後はずっと世間に「ヤダあの人、こんな文化的砂漠みたいなリストをあてがわれてサブカルにもなれずに死にゆく定めの、っていうか単純に冴えない人?みたいな?」と後ろ指さされながら生きていくしかない貴方様が手にするべきものは上の2つ以外にはありません。ないのです。そう、誰も恐怖からは逃れられないのだ。わっはっは。お逃げなさい。お逃げなさい。逃亡者の街へ。

Popularity: 2% [?]