2013/04/23

NDK r8e でTitanium Mobile Android Module がビルドできない

Titanium SDKをソースから作成する場合に、NDK r8e (2013/04/22時点で最新)を使ってビルドすると、うまくいかない件が報告されています。
Correcting a Bug in the Latest Google NDK r8e

NDK r8eは、64ビット環境に対応した初めてのバージョンで、64ビット環境でビルド時間の短縮が期待されるものです。それに従って各プラットフォームで32ビット版と64ビット版の2種類が提供されています。割と大きな変更だったために、Titaniumに限らず、いろいろなところでバグが報告されています。

Ti SDK をビルドせず、単にTi アプリをビルドするだけであれば、NDKのバージョンや修正はほぼ関係ないようなのです。しかし、Android用のTi モジュールを作成する場合は、更に問題があるようです。

Ti SDK 3.1.0 GAより以前 (3.0.2.GA,3.0.0.GA,2.1.4.GA ... ) でモジュールをビルドする場合、上記の修正をしてもしなくても、NDK r8eの32/64ビット版のいずれでも以下のような結果になります。
     [exec] /usr/include/machine/_types.h:34:24: fatal error: arm/_types.h: No such file or directory
     [exec] compilation terminated.
     [exec] make: *** [/var/folders/r6/fpdnpcys5w9gfvm95dynn6v00000gn/T//k-ishida/tts-generated/obj/local/armeabi/objs/jp.isisredirect.tts/jp.isisredirect.tts.TtsModule.o] Error 1

BUILD FAILED
/Library/Application Support/Titanium/mobilesdk/osx/3.0.0.GA/module/android/build.xml:371: The following error occurred while executing this line:
/Library/Application Support/Titanium/mobilesdk/osx/3.0.0.GA/module/android/build.xml:326: exec returned: 2

また、Ti SDK 3.1.0.GAでビルドする場合には、上記の修正をした場合に限って、以下のような結果になります。


ndk.build.local:

compile:
    [javac] Compiling 3 source files to /Users/k-ishida/Documents/TitModuleWorkspace/tts/build/classes
    [javac] /Users/k-ishida/Documents/TitModuleWorkspace/tts/build/generated/java/jp/isisredirect/tts/TtsModulePrototype.java:10: package org.mozilla.javascript does not exist
    [javac] import org.mozilla.javascript.Context;
    [javac]                              ^
    …..
    [javac]                                   ^
    [javac] 100 errors


以上をふまえると、現状のTi SDKでAndroid用モジュールをビルドするには、NDK r8d まででビルドするのが妥当なようです。



2013/04/17

(続)Titanium module Android の国際化リソースはどう配置すべきなのか?

前回のに続いて、2番目の assets ディレクトリに国際化リソースを配置する場合について検討する。


まず、モジュールプロジェクト内で
assets/
      values/
            strings.xml
      values-ja/
            strings.xml
のように、言語リソースを配置しておくと、ビルド後のモジュール内では jar の中に統合され、最終的にモジュールを読み込んだ Ti アプリ内の
/assets/
以下にそのまま配置される。

Android アプリにおいて、assetsディレクトリは、いわゆるリソースを置くresディレクトリに対して、読み取り専用のローデータの置き場所とされている。例えば、sqliteのもとのDBなんかを置いておいて、アプリ起動時に書き込み可能な領域 ( 例えば、/data/data/{パッケージ名}/app_data/ )にコピーしたり、固定的な画像やhtmlなどを置くのに使われる。
読み出しには通常のFileではなく、一部の例外を除いて、AssetsManager経由での読み取りになる。

ということは、assetsディレクトリの国際化リソースを置いた場合には、Android OS のリソースハンドリングの支援は使えず、全て自前で(!?)処理しなければならないことになる。例えば、/assets/values/string.xml , /assets/values-ja/strings.xml などのリソースファイルから所望の実行時のロケールに対応した文字列を得るには、少なくとも
  isis AssetsStrings.java
のようなことになる(もちろん、これはかなり粗い実装です)。

strings.xml だけ相手にすればいいのならまだしも、レイアウト関連の xml などに埋め込まれた文字列リソースにも対応するとなると、相当面倒なことになる。やってやれないこともないだろうが、車輪の再発明どころの騒ぎではなくなることになるだろう。

ということで、Android の場合、国際化リソースを /assets/ に置くのは特別な理由がない限り、選択肢に入れなくてよいだろう。




2013/04/14

Titanium module Android の国際化リソースはどう配置すべきなのか?

前回のTitanium module ios の国際化リソースはどう配置すべきなのか?に続いて、Androidの場合には、どうすべきなのか調べた。

アプローチとしては、
1)モジュールのプロジェクトディレクトリの
/platform/android/res/
に言語リソースを配置する。
2)モジュールのプロジェクトディレクトリの
/assets/
に言語リソースを配置する。
の2通りが考えられる。

まず、1)の方法では、
/platform/android/res/
      values/
            strings.xml
      values-ja/
            strings.xml
のように、言語リソースを配置しておくと、モジュールを読み込んだTiアプリ内の
/res/
以下に配置される。これは、Androidアプリの通常の言語リソースの配置場所なので、プログラムからは、
mContext.getString(R.string.rate_title)
のようにR経由で取得するのがAndroidアプリの通常のやり方だ。
しかし、モジュールの場合、platformディレクトリ内のリソースから、R.javaは、生成されないので、このままでは、コンパイルエラーとなる。
そうした場合、Tiには便利な方法が用意してあって、
mContext.getString(TiRHelper.getApplicationResource("string.rate_message")
の様にTiRHelper#getApplicationResource()を使用して、リソースIDを文字列として与えておいて、Tiアプリ実行時に解決することができる。

これで、無事言語リソースを同梱したモジュールが完成する訳だが、このままだと実用にならない。
i18nを持つTiアプリの場合、ビルド過程で、アプリ側で指定した言語リソースとモジュールの言語リソースが競合してしまうことがある。例えば、

Ti アプリのプロジェクト
/i18n/
    en/
       strings.xml
    ja/
       strings.xml

の様にTi アプリ側の言語リソースがあると、


values/
      strings.xml  <---- アプリケーション由来
      styles.xml
      theme.xml
values-ja/
      strings.xml  <---- アプリケーション由来
の様になってしまう。strings.xmlのマージは行われず、モジュールのstrings.xmlは欠落している。
こうなってくると、上記のTiRHelper#getApplicationResource()に与える引数に対応するものが存在しない状態になってしまう。

解決方法としては、モジュールに同梱するplatformディレクトリ内の言語リソースにあらかじめモジュールを示す接頭辞をつけて置くことだ。これでファイルとしての競合は避けられる。
/platform/android/res/
      values/
            <module name>strings.xml
      values-ja/
            <module name>strings.xml

TiRHelper#getApplicationResource()に与える引数は、xmlで指定された<resources/><string/>のidなので、変更せず

mContext.getString(TiRHelper.

getApplicationResource("string.rate_message")


のままで良い。

言語リソースに限らず、platformディレクトリ内で、モジュール側から呼ばなければならないリソースについては、この方法をとったほうが、競合の問題を避けることができると思われる。

さて、配置問題は解決したけれど、モジュールのユーザが文字列リソースとして、モジュール内で使われているidと競合するidを使う場合はあり得る。その場合は、Ti アプリビルド時にビルドエラーを引き起こすだろう。
[exec] [ERROR] ..(省略)..appirater/build/android/res/values/strings.xml:4: error: Resource entry rate_title is already defined.
[exec] [ERROR] ..(省略)..appirater/build/android/res/values/appiraterstrings.xml:3: Originally defined here.
[exec] [ERROR] Error generating R.java from manifest
[exec] [ERROR] Build Failed.
よくログを読めば、わかることだが、モジュールユーザーにあらかじめモジュール内で使用しているidを知らせておく必要があるかもしれない。

少し長くなったので2)については項を改めて。

1)の方法を取り入れたモジュールの作例として

isis / TiAppiraterAndroid
を作ったので、参考にされたし。






2013/04/12

Titanium module ios の国際化リソースはどう配置すべきなのか?

昨日@k0sukeyさんのTiAppirater を i18n するよ!に触発されて、ios用Titanium Moduleへのリソース、特に国際化リソース周りについて調べ始めた。

はじめ、@k0sukeyさんの方法とは違って、モジュールのプロジェクトディレクトリの"assets"に国際化リソースをほうりこめば、Titaniumがよろしくやってくれるのではないかと考えたが、そうはいかなかった。
確かに"assets"に置いておけば、配布用のモジュールに必要なリソースが同梱されるし、モジュールを読み込んだTi アプリにも同梱される。しかし、それらは、アプリが通常参照するリソースバンドルではなく、
modules/<module id>/
以下に配置されて、イメージなど、モジュールの他のassetsリソースと同様に扱われてしまう。その結果、モジュール側から、
NSLocalizedStringFromTable
しても、見に行く場所が違うので、空振りに終わる訳だ。

解決策の方向は2通りあるだろう
1)builder.py, localecompiler.py等々、ロケール関係のモジュールのビルド方法を修正して"assets"に国際化リソースがある場合は、アプリケーションの国際化リソースにマージするように修正する
2)モジュール内から国際化リソースを参照する場合には、modules/<module id>/から参照するように、NSLocalizedStringFromTableInBundleを使用する

1)は、短期間では、ちょっと手に負えないので、2)の方針を元に修正を加えてみた。
isis/TiAppirater
とりあえず、動いているようだ。

しかし元になるライブラリがソースで提供されている場合は、ソースを修正すれば何とかなるが、バイナリ提供の場合、2)の方法は使えない。

根本的には、1)の方法があってしかるべきだと思う。
あるいは、ios環境でのライブラリ提供の際の、国際化リソースの明確なルールがどこかにないものだろうか ....

注)
"platform"ディレクトリに置いても、ios用モジュールの場合何の効果もないようだ。モジュール開発に関しては、このディレクトリはAndroid専用といえるのかもしれない。

注)
現在いろいろなところで提供されているモジュールがこうした国際化などの細かい点が考慮されていないことが多いのはとても残念なことだ。




2013/04/07

Test Titanium Android App on emulator using Intel x86 Atom System Image

(先日Appcelerator Developer Blogで、
Configuring Appcelerator Titanium to Use Intel x86 Images
としてx86エミュレータを使う方法が説明されました。そこでは主にコマンドラインからの使用法を解説されています。ここでは、それとは独立にTitanium Studioから使う場合のハマりそうなポイントを書いておきます)。

Intel x86 AtomベースのAndroid端末の登場とともに、Intelは、開発ツール群そろえてくれていて、x86 Atom用のAndroid エミュレータも提供しています。そのエミュレータが比較的高速に動くので、テストやデバックの効率が良くなります。
x86 Android端末のお披露目のときに伺った話では、x86 Android端末では、ARMベースのネイティブコードを含むAppも「Intelのスーパーテクノロジでエミュレートできる」とのことです(「スーパーテクノロジ」がなんなのかは、開発担当者しかわからないとのことでしたが。。)もしかするとx86エミュレータでも、v8エンジンなどのARMネイティブコードを含むTitanium mobile Appの開発に使えるかもしれないと淡い期待を抱きます(Titanium SDK 2.1.0GA以降x86のネイティブコードが追加されています 捕捉参照)。
Javaによる開発では、x86 エミュレータでさくさく開発できるのですが、Titaniumで使うとうまくいかない状況が続いていました。Titanium SDK 2.xの場合、エミュレータにAndroid (x86) を指定しても、ARM エミュレータが立ち上がっていましたが、Titanium SDK 3.0.0.GA以降は、正しくx86 エミュレータが起動します。


必要なことは
1) AVDマネージャからIntel x86 Atom System Imageを導入
2) Google謹製のGoogle Map API (com.google.android.maps) を導入したsystem.imgを作成
です。

1)はいろいろなサイトで紹介があるので、ここを参考に導入されると良いでしょう。
Titanium Studioから導入する場合には、Android SDK Managerを起動するために、Java Perspective に切り替える必要があります([Window > Open Perspective > Other ... > Java])。これ以外の点は特に問題ないでしょう。
Android SDK の extras にある Intel Hardware Accelerated Execution Manager の導入もお忘れなく。


2)は、配布されているx86のsystem.imgにGoogle Map API が含まれていないためで、TitaiumからGoogle Map API を使用していないのであれば、必要ありません。


OSX 10.7.5
Titanium Studio 3.0.3.201302141237
Titanium SDK 3.0.0.GA
Android SDK tool 21.1 / Android SDK platform-tools 16.0.1

Android SDK 4.2 (API Level 17)のエミュレータを例にGoogle Map APIの導入手順を示します。

●下準備その1
Androidエミュレータ内のsystemディレクトリを修正するとき、”Out of Memory"エラーにならないように、Titanium Studioからエミュレータを起動する場合、Titanium SDKのbuilder.pyの一部を修正して、エミュレータの起動オプション-partition-sizeの値を増やしておく必要があります。

Titanium SDK 3.0.0GAの場合 /android/builder.pyのL.593あたりを以下のように書き変えておきます。


'-partition-size','512' #'128' # in between nexusone and droid


他のTitanium SDKを使用する場合には、'-partition-size'で検索して該当箇所を修正してください。この作業は、system.img作成/導入以降には必要ないものなので、作業後もとにもどしておくのも良いでしょう。

●下準備その2
system.imgファイルを作成するためのツールmkfs.yaffs2.x86を用意します。

Download:mkfs.yaffs2.x86
からダウンロードしておきます。


●最初に、Google APIs 4.2 [ARM] のエミュレータからcom.google.android.maps関連のファイルを抜き取ります。

step1 Google APIs 4.2 のエミュレータ [ARM] を起動します。
Titaium Studioから起動するには以下のようにな設定してRunすることになります。



step2 OSX側に適当な保存先フォルダを用意します。
terminalで、先ほどの保存先フォルダに移動して、adb pullコマンドで2つのファイルをOSX側にコピーします。
$ adb pull /system/etc/permissions/com.google.android.maps.xml
122 KB/s (816 bytes in 0.006s)
$ adb pull /system/framework/com.google.android.maps.jar
949 KB/s (157310 bytes in 0.161s)

step3 コピーが終了したら、Google APIs 4.2 のエミュレータを終了します。

●次にx86 エミュレータを起動して、Google Map APIを導入したsystem.imgを作成します。

step1 Android 4.2 [x86] のエミュレータを起動します。
Titaium Studioから起動するには以下のようにな設定してRunすることになります。




step2 systemディレクトリを書き込み可能にする
terminalで、adb shell コマンドを使用します。
$ adb shell mount -o remount,rw /system

step3 Google Map API関連のファイルを書き込む
抜き取っておいたファイルをadb pushします。
$ adb push com.google.android.maps.xml /system/etc/permissions
144 KB/s (816 bytes in 0.005s)
$ adb push com.google.android.maps.jar /system/framework
2554 KB/s (157310 bytes in 0.060s)

step4 イメージ作成ツールを/dataに書き込み実行可能にする
ダウンロードしておいたmkfs.yaffs2.x86を書き込んで、実行できるようにします。
$ adb push mkfs.yaffs2.x86 /data
351 KB/s (441952 bytes in 1.229s)
$ adb shell chmod 777 /data/mkfs.yaffs2.x86

step5 イメージファイルを作成
mkfs.yaffs2.x86を実行して、/systemのイメージファイルを作成します。
$ adb shell /data/mkfs.yaffs2.x86 /system /data/system.img
mkfs.yaffs2: Android YAFFS2 Tool,Build by PowerGUI
   at http://www.openhandsetalliance.org.cn
Building...
Build Ok.

step6 出来上がったイメージファイルをOSX側の保存先にコピー
$ adb pull /data/system.img
505 KB/s (285922560 bytes in 552.216s)

200M強あるので、かなり時間がかかります。

●Android SDKディレクトリにあるシステムイメージの保存場所に上記のsystem.imgをコピーします。
コピー先は、Android SDK 4.2 (API Level 17)のx86用なので、
<android-sdk>/system-images/android-17/x86/system.img
になります。


これで、次回のエミュレータ起動からGoogle Map API導入済みのsystem.imgが使用されます。

以上で、Kitchen Sinkも動くようになりました。


問題点

さくさく動きはするのですが、いくつか問題があります。

その1 adb コネクションが切れやすいんじゃないか?

Titanium Studioに限ったことじゃないけど、adbのコネクション切れの確率が高いようです。



その2 モジュールにx86用のネイティブコードがない場合がある(というか大抵対応してない)


Titanium SDK 2.1.0より前のSDKで生成されるモジュールプロジェクトは、x86用のコードを生成してくれません。これを生成できるようになるのはTitanium SDK 2.1.0以降です。
マーケットプレースにある多くのモジュールは2.1.0以前の環境用につくられているのため、それらを使用したTitanium アプリケーションは、x86用v8とのグルーコードがないので起動時にロードエラーを起こします。

このあたりは、モジュール開発者の対応待ちです。
(モジュールのバージョン番号のポリシーが破綻してしまっているけど ..... あまり深く考えないでおこう)

(捕捉)
Titanium SDK 2.1.0GAから、ARM用のネイティブコードに加えてx86用のものがlibsに追加されて、Titanium アプリはx86対応となっているようです(ということは、x86サポートしなくても良いなら、libsから削除すれば、ダイエットできるってことです)。
しかし、x86 エミュレータを起動する機能はこの時点では追加されておらず、3.0.0GAで初めてx86エミュレータの起動機能が追加されました。

(捕捉2)
当初、x86 Atom system image は、Intel により提供された2.3.3(API Level 10)と4.0.3(API Level 15)だけでした。4.0.3のほうが安定しているとされており、2.3.3は、「がんばって安定させます」とのことでした。
その後、API Level 16,17も提供されるようになりました。
また、Android Sourceからビルドして作成することもできるようになっています。

(参考)
Installing the Intel Atom Android x86 Emulator Image Add-on from the Android SDK Manager
Using the Android Emulator | Android Developers
How to use Google Maps API in Android emulator SDK version 17

2013/04/01

Android TextToSpeech#setLanguage() の使い方

前回は TextToSpeech の初期化について書いたが、N2 TTS を使用時に女性/男性の切り替えができないと書いた。可能な方法が見つかったので、書いておく。


まず、ロケール周りのテキスト表現が2種類(大文字小文字も含めると更に多い)ある。言語と国を"-"で区切る場合と、"_"で区切る場合があり、TTSでの取り扱いでは、これが混在している。例えば、ACTION_CHECK_TTS_DATA で取得できるサポート言語のリストでは "-"区切りとなるのに対して、使用中のTTSエンジンの現在の言語設定を取得するgetLanguage()では、"_"区切りとなっている。
結局 Lacale のインスタンスを作って操作することになるので、なんでもいいんだが、無駄な混乱の元になるといけないので、どちらか一方に正規化して整理したほうがコードのメンテナンスは楽になるだろうと思う。区切り文字が何になるかは、運(?!)次第な面があるので、OSのアップデートなんかを考えると、ぜひともそうしておいたほうが、精神衛生上よろしいと思う。


さて、問題の N2 TTS エンジンなのだが、ACTION_CHECK_TTS_DATA で取得できる言語は

jpn-JPN-F01
jpn-JPN-M01
の二つだ。Android 組み込みの Google TTS エンジンの場合は、
fra-fra
eng-gbr
eng-usa
ita-ita
deu-deu
spa-esp
のように、ロケールの Variant にあたる部分がないし、全て小文字になっている。
(という具合に表現と要素まで違ってくるので、この違いに注意して、実装することが必要)。
現象として、Google TTS エンジンから取得した言語情報を使えば、言語の切り替えできるが、N2 TTS からの場合、切り替えできないということになるが、エンジン側としては、指定どおり切り替えている( setLanguage() の戻り値で調べられる)。N2 TTS で問題になるのは、実は3項目目の Variant にあたる部分で、これが

new Locale("jpn-JPN-M01");


の際、なくなってしまうのが、原因だと判明した。原因が分かれば簡単で、あらかじめ言語文字列を3要素に分解して
new Locale("jpn", "JPN", "M01");
のようにして、3要素指定でインスタンスを作ればいい。
これを setLanguage() してあげれば、めでたく男性に切り替わってくれる。


ところで、Variant 要素を具体的にどういう表現にすればいいのかといったことに関して、Android OSのリファレンスにも特に規定はないようだ。唯一http://developer.android.com/reference/android/speech/tts/TextToSpeech.Engine.html 
にvariantの例として、
"eng-USA-FEMALE"
というのがあげらている。試しに
"eng-USA-MALE"
をしてみたが、女性のままだった。しかし
"eng-gbr-MALE"
を試すと、あら不思議、大英帝国のジェントルマンの声が聞こえてきた。


という訳で、variantの値が、具体的にどうなるのかは、手探りの状態だ。


どうにかならないのかな???