2013/07/31

Titanium Mobile Android ダイエット

先日Twitter上で、Android 用の Ti アプリのサイズが大きいのでなんとかしたいって話があって、@uk1124
[Titanium]APKのサイズを小さくする方法で結果をレポートしてくださった。
今回はこの補足です。

まず、@isis331 のいうandroid/builder.pyを修正する方法では、ディストリビューション用のapkでは何の効果もありません。
なぜかというと、Ti SDK 3.1.1 GAで生成されるディストリビューションのパッケージには、デフォルトでは、ARMv5(armeabi)とARMv7(armeabi-v7a)のコードしか含まれないからです。
では、x86を含めるにはどうするかというと@infosiaさんの指摘されたtiapp.xml の<abi>で、allを指定する必要があります。これで、ARM用2つに加えてx86も含まれることになります。
反対にarmeabiだけを指定すれば最小サイズのものが出来上がります。

さて、android/builder.pyを修正する方法が有効なのは、ディトリビューション用ではなく手元でテストする場合に生成されるapkに対してです。"Install to Android Device"等のコマンドで、ビルドされたときには、全てのabiが含まれた状態でビルドされます。この場合には、android/builder.pyを修正して、必要なabiだけ配列に入れておけば、最小限のサイズのapkが生成されます(x86を省けば、8.7Mから6.2M程度になります)。
デバイスへの転送速度が気になる向きには、有効な手段かも知れません。

(注)
apkの中身は、zipとして解凍してlibs以下のフォルダをみれば、確認できます。



2013/07/15

RoboVM first step

RoboVM は、Java で書かれたコードを iOS, Mac OS X, Linux 上で実行するための環境を提供してくれます。VM という名前になっていますが、新手の Java Virtual Machine ではありません。Java バイトコードをネイティブコードにトランスレートするコンパイラがその実体です。実行時には、ネイティブコードだけのアプリが実行されることになります。
Java 由来の API が簡単に使えたり、Objective-C へのブリッジもあるので、Javaコードから iOS のフレームワークを呼び出すことも可能です。
現在リリース版のバージョンが 0.0.2 ということで、出来立ての状態ですが、仕組みがおもしろいのと、Android アプリとのコードの共通化への取り組みの一環として、非常に興味深い環境です。

導入方法は
RoboVMのIntroduction

Hacking on RoboVM / Building from source
にありますが、若干記述が古くなっています。
Mac OS X 10.7 での、導入から、Hello, world までのはまりどころを以下にメモしておきます。

RoboVM が使うコンパイラ環境の導入


Java SE JDK 7
Xcode 4.6.2 + command line tools
LLVM 3.2
が必要です。
Java SE JDK 7 については Oracle のサイトからインストーラをダウンロードして導入しますが、それだけだと、OS X 10.7 に付属の Apple 提供の JDK 6 を指したままになっているので、適宜 ~/.bash_profileに

export JAVA_HOME=export JAVA_HOME=$(/usr/libexec/java_home)

などとして、JDK 7 を指すように変更が必要です。

LLVM については、homebrew などでインストールされたものだと、RoboVM がコンパイルするときなぞのエラーとなります。
RoboVM は、
clang+llvm-3.2-x86_64-apple-darwin11.tar.gz
を仮定しています(これ以外だとうまく動作しないようです)ので、素直に RoboVM の Introduction の指示に従って、導入するのが良いでしょう。RoboVM が /opt/ 以下の llvm を見に行くので、シンボリックリンクとして
/opt/llvm
を作成しておかなければなりません。このディレクトリは homebrew の管理とは無関係なので、RoboVM 専用だと思っておけば、問題は生じないでしょう(homebrew の場合、/usr/local/ 内に /usr/local/Cellar/ へのシンボリックリンクを作って管理しています)。
Xcode についての注意点として、複数の Xcode を導入して Xcode.app のアプリ名をXcode462.app などとリネームしている場合です。この場合、iPhone シミュレータが起動できないなど、不具合を生じるので、Xcode.app という名前のままである必要があります。

tar.gzからの導入

Download the latest RoboVM release から tar.gz をダウンロードして、/opt/ 以下の展開すれば良いのですが、本家の説明では、バージョン番号毎に管理して /opt/robovm としてシンボリックリンクを作ることになっています。しかし、これだとうまく働きません。現行バージョン(0.0.2)では実行時にシンボリックリンクの解決ができていないようなので、 /opt/robovm 以下に bin, lib, license の各ディレクトリの実体が来るように展開します。

IDEの利用/Eclipse プラグインの導入

RoboVM での開発には Eclipse が使えて便利です。
http://download.robovm.org/eclipse/
から導入できますので、Help >Install New Software から導入します。

ソースからの導入

より本格的には、ソースから導入していろいろ実験的なことをするのもいいでしょう。

Git
CMake 2.8.8
Maven 3
が必要です。
本家の説明では、
JDK 6 or newer
が必要となっていますが、JDK 6 の場合は、途中でコンパイラーがパースエラーを起こしていまします。JDK 7 を使うのがいいでしょう。
ソースは
robovm/robovm
から入手できます。
working copy のディレクトリから
vm/build.sh

でライブラリが生成できます。
mvn clean install

でインストール用のコンポーネントが生成できますが、pom.xml が JDK 6 用になっています。先に述べたようにパースエラーとなりますので、
      <plugin>
        <groupid>org.apache.maven.plugins</groupid>
        <artifactid>maven-compiler-plugin</artifactid>
        <version>2.3.2</version>
        <configuration>
          <source></source>1.7
          <target>1.7</target>
          <debug>true</debug>
          <optimize>true</optimize>
          <showdeprecations>true</showdeprecations>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>

の "1.6"の箇所を"1.7"と修正の上実行します。
dist/target/ 以下に
robovm-0.0.3-SNAPSHOT.tar.gz
ができますので、これを”tar.gzからの導入”の手順で導入します。

RoboVM 本体と Eclipse のプラグインのバージョンが一致している必要があるようなので、あわせて、プラグインもビルドして導入する必要があります。
robovm/robovm-eclipseからソースを入手し、
mvn clean install
でビルドできます。

Hello World

Eclipse の File > Project ... から RoboVM > RoboVM Cocoa Touch Project を選択します。


プロジェクト名を"HelloWorld"などとします。

src が空のプロジェクトが作成されますので、メインクラスを作成します。メインクラスの名前はプロジェクト名と同じものでなければなりません(パッケージはdefault packageを使用します)。


メインクラスのスーパークラスを

org.robovm.cocoatouch.uikit.UIApplicationDelegate.Adapter
とするのが RoboVM アプリのお作法です。ちょうど、Objective-C での開発で UIApplicationDelegate の サブクラスを作成しなければならないことに対応しています。

HolloWorld.javaの中身はたとえば以下のようにします。

import org.robovm.cocoatouch.coregraphics.*;
import org.robovm.cocoatouch.foundation.*;
import org.robovm.cocoatouch.uikit.*;

import org.robovm.cocoatouch.uikit.UIApplicationDelegate.Adapter;


public class HelloWorld extends Adapter {

    private UIWindow window = null;
    private int clickCount = 0;

    @Override
    public boolean didFinishLaunching(UIApplication application,
            NSDictionary launchOptions) {

        final UILabel label = new UILabel();
        label.setFrame(new CGRect(70.0f, 121.0f, 191.0f, 37.0f));
        label.setText("Hello World!");
        label.setBackgroundColor(UIColor.clearColor());
        label.setOpaque(true);


        window = new UIWindow(UIScreen.getMainScreen().getBounds());
        window.setBackgroundColor(UIColor.lightGrayColor());
        window.addSubview(label);
        window.makeKeyAndVisible();

        return true;
    }

    public static void main(String[] args) {
        NSAutoreleasePool pool = new NSAutoreleasePool();
        UIApplication.main(args, null, HelloWorld.class);
        pool.drain();
    }
}

実行するには、プロジェクトのサブメニューから Run > から実行環境を選びます。
初回は、ビルドなどに時間がかかるようですが、2回目以降は若干時間がかかるものの耐えられないほどではありません。
iOS Simulator App (iPhone)での結果は以下のようになります。



さて、環境が整ったので、暇を見て本格的なアプリを作ってみようと思います。


2013/05/27

Titanium SDK をMIPS 対応にする(完成か?)

Titanium SDK のMIPS対応の続報です。


前回問題になっていた
libtiverify
libtiprofiler
のMIPS版が存在しない問題ですが、ないなら使わないようにSDKを修正することで対処可能です。今のところこれらがなくても、作成されたアプリは支障なく動作するようです。libtiverifyはおそらくTi アプリ起動時にライセンス等々を確認するためのもの、libprofilerはTiアプリのプロフィリングのためのものだと思われます。

その他、いろいろワーニングがでている箇所を、新しいV8のお作法通りにきれいにして、これで、無事解決と思ったのですが、そうはいかず、ハマってしまいました。

全般的に何となく動いているのですが、Ti.Media.vibrate()など、オブジェクトに対するメソッドを呼んだ場合、実行時に
illegal invocation
になって、肝心の登録された関数本体が呼ばれなくなっていました。あれこれ原因を探るのに時間を浪費してしまいました。
しかし、
[v8] r13131 committed - Change deprecated semantics of function template signatures....
によれば、新しいV8では、元になるFunctionTemplateのプロトタイプがHiddenでなければならないという仕様になったとのことです。

ということで、この修正を加えて、無事オブジェクトに対するメソッドも完動するようになりました。
これで、外部モジュール以外の部分は、問題なく動作するようになっていると思います。
MIPS以外の端末でも、新しいV8の効果が期待できるので、皆さんお試しください。


ソースコード
https://github.com/isis/v8_titanium/tree/gyp
https://github.com/isis/titanium_mobile/tree/mips
バイナリ(テスト用)
https://www.dropbox.com/s/3gn0qfdjzcwd1ld/mobilesdk-3.2.0-osx.zip


2013/05/19

Titanium SDK をMIPS 対応にする(研究中)

MIPSベースのAndroid端末がかなりの数に上るのに、Titanium Mobileは対応していません。とても残念なので、なんとかしようとトライしています。まだ研究途中ですが、Titanium SDKのビルドまではできる状態になったので、情報を公開しておきます。
(いろいろツール類のインストール作業等を伴いますが、うるさくなるのでそれらの説明は割愛します)


歴史


* Android SDK のMIPS対応は API Level 9以降。
* Googleの公式NDKがMIPS対応したのは r8 (May 2012) 以降。
* V8 (https://code.google.com/p/v8/)がMIPS対応したのは、3.7以降だが、いろいろ問題があったので、V8 for MIPS (paul99 / v8m-rb) というプロジェクトが存在し、現在はこちらのコードが本家に適応されている。 
* V8 のビルドシステムはsconsベースからgypベースに変更されました。
* Titanium SDK の使用しているV8のバージョンは3.9.24.29で、Acceleratorが作ったTitanium Mobile 用のsconsベースのビルドツール (appcelerator / v8_titanium) を使って作成される。


v8_titaniumのビルド


v8 のライブラリの作成ができないことには、何もはじまらないので、まず、これに取りかかります。
問題は、AppceleratorがTi SDKに導入しているV8 v3.9.23.24 は古いので、そのままではMIPS対応できないようだということです。そして、新しいV8 はsconsベースではなくてgypベースのビルドシステムになっているので、v8_titaniumのビルドツールの修正が必要だということです。 しばらく自力でがんばろうとしましたが、世の中よくしたもので、既にgypベースのV8に対応するブランチを作成しているプロジェクトがありました。
     joshthecoder / v8_titanium
のgypブランチを使えば、最新のV8からもビルドできます。このブランチを元にMIPS対応を付け加えたブランチを作成しました。
     isis / v8_titanium
のgypブランチです。 リポジトリ取得後gypブランチをチェックアウトしてください。

V8のサブモジュールは、3.9.23.24を指しているので、最新版(2013/05/18時点で3.19.3)をフェッチしてください。

ビルドには build_v8.shを使います。
./build_v8.sh -j 4 -p android-9 -l all -s on 

とすると、armeabi armeabi-v7a x86 mipsel という4つのアーキテクチャ毎のライブラリが作成されます(ビルド時間はかなりかかります)。
./build_v8.sh -t 

とすると、上記のライブラリと必要な情報ファイルを含むtarballが作成されます。


Titanium SDKの修正


やらなければならないのは
  • MIPSアーキテクチャーの追加
  • 独自ビルドV8 ライブラリの導入
  • V8 のNew APIに対応
です。 
titanium_mobileの scons をたどって、適宜 mips を追加していきます。
Appcelerator の提供する titanium_mobile では、ビルド時に、V8 のライブラリをサーバーから取得し導入するようになっています。まずこの機能を削除して、手動で独自ビルドのV8 ライブラリを導入できるようにしなければなりません。
   <titanium_mobile>/android/build/common.xml

<condition property="libv8.latest">
<equals arg1="${libv8.current.version}" arg2="${libv8.version}"/>
</condition><antcall target="download.libv8"/>
<antcall target="download.libv8"/>

の部分をコメントアウトしておきます。

次にV8 ライブラリを
<titanium_mobile>/dist/android/libv8/

以下に展開します。ライブラリの tarbar を展開すると、必要な配置と違っているので、手動で以下のように配置します。V8のバージョン番号を示すディレクトリを起点にして
<titanium_mobile>/dist/android/libv8/
    3.19.3/
        release/
            libv8.json
            include/
            libs/
               mips/
               x86/
               armeabi-v7a/
               armeabi/

のようになります(gitからフェッチした直後はdistディレクトリ以下は存在しないので自前でディレクトリを作成する必要があります)。
libs以下の各ディレクトリにはアーキテクチャー毎のライブラリを配置します。
新しいV8のライブラリは、旧来1つのlibv8.aとして生成されていたのが、base, snapshot,nonsnapshotと3つのファイルに分かれて生成されるようになりました。Titanium で必要なbaseとsnapushotのライブラリを追加します。baseについては、ファイル名がアーキテクチャ名つきで生成されるので(なぜ?)、面倒なので、リネームします。また、V8生成時にMIPS用はmipsel (MIPSのリトルエンディアン)として生成されますが、titanium_mobileのビルドシステムを修正するのは面倒なので、mipsという名前に修正しておきます。mips/x86/armeabi-v7a/armeabi/ には、それぞれ、少なくとも
libv8_base.a
libv8_snapshot.a

2つのファイルが必要です。
sconsからたどって、libv8のバージョン番号の指定も適宜修正しておく必要があります。
V8のAPIがかわったので、Titanium からV8を呼び出している箇所で、コンパイルエラーがでます。New APIに置き換えてやる必要があります。
一応、ビルドが通る程度には置き換えてありますが、置き換えた正しいか検証はこれからです。
v8::Stringは非推奨になりましたが、現状ままにしてあります(なのでワーニングが大量にでます)。
以上まとめたものは、
isis / titanium_mobile
のmipsブランチに置いてあります。
ところで、出来上がったTitanium SDKを展開してみると、mipsのNDK関連のディレクトリに、
libtiverify
libtiprofiler
が欠けているのがわかります。これらのライブラリはTitanium Mobileのオープンソース部分ではないので、ビルド時に生成されません。このままの状態で出来上がったSDKでMIPSアプリを作成すると、起動時にクラッシュすると思われます。
どうしたものでしょうね ....
現時点では、最新のV8をもとにMIPS対応を含んだTitanium SDKのビルドができるようになった段階です。それを元に作成したアプリを検証等していません。
MIPSに対応しないとしても、新しいV8を利用できるのでJavaScriptの実行は高速になると期待できます(ただし実行コードのサイズも大きくなりいます)。
これを元に多くの方がトライされることを期待します。

2013/05/15

Titanium Mobile Tizen Alloy でエミュレータでアプリが起動しない

Titanium SDK 3.1.0.GA の最初の時点では、Alloy ベースのアプリを Tizen 向けにビルド、エミュレータ実行することができたのですが、最近試したところ、

[INFO] :   compressing ok
[INFO] :   Tizen SDK found at: /Users/k-ishida/tizen-sdk
[INFO] :   Executing: /Users/k-ishida/tizen-sdk/tools/ide/bin/web-uninstall -t 10 -i 62WvUifqkC --device=emulator-26100
[INFO] :   Uninstall failed, '62WvUifqkC' widget is not installed.
[INFO] :   CLI command failed with error output:
[INFO] :   
[INFO] :   Executing: /Users/k-ishida/tizen-sdk/tools/ide/bin/web-install -t 10  --widget="/Users/k-ishida/Documents/AlloyProj/test/build/tizen/tizenapp.wgt" --device=emulator-26100
[INFO] :   'tizenapp.wgt' file transfer successful.
Enable developer mode.
'62WvUifqkC' install successful.
[INFO] :   Executing: /Users/k-ishida/tizen-sdk/tools/ide/bin/web-run -t 10 -i 62WvUifqkC --device=emulator-26100'62WvUifqkC' launch failed.
[INFO] :   CLI command failed with error output:
[INFO] :   
[INFO] :   Project built successfully in 13s 428ms

となって、転送には成功したことになっているのですが、アプリ起動失敗といわれてしまいます。しかし実際は転送はうまくいってもエミュレータへのインストールがうまくいっていないようです(tizen sdk の web-list コマンドで確認できます)。

いろいろ調べた結果、作成される wgt ファイルに . のついた不可視ファイルがある場合、wgt ファイルが転送されるものの、インストールできない、にもかかわらずweb-install コマンドはそれを報告できない ... ということが判明しました。

で、なぜに、いつの間にかTi側で隠しファイルができてしまうか?
Alloy プロジェクト作成時に Alloy のプロジェクトディレクトリで
/app/assets/blackberry/.npmignore
というファイルが作成されています。
必要ないよね、これ。

Alloy プロジェクトを作成したら blackberry のディレクトリごと削除しておけば、問題なくなります。

注)Ti SDK 3.1.0.GA のリリース以降どの時点でblackberryディレクトリが作成されてしまうようになったのかは、気力がないので、誰か確認してください。



2013/05/04

Titanium Alloyのカスタムwidgetで、Androidでは、addEventListenerがエラーになる

Alloy である程度まとまった view を作るのには、独自の widgetを 作るのが便利だと思われます。
Titanium Studio でプロジェクトを右クリックして、New > Alloy widget を選択して、新しい widget を作成できます。
CLI では、プロジェクトのルートディレクトリで、
$ alloy generate widget NAME
とすると、同様に作成できます。

さて、この widget に利用する側からイベントを追加したいのですが、通常の view に対してと同じように
$.mywidget.on( EVENTNAME, FUNCTION);
を使っても、iOSでは何の効果もありません。
Android では、
05-04 11:45:58.802: E/V8Exception(4776): Exception occurred at ti:/events.js:172: Uncaught TypeError: Object #<Controller> has no method '_hasListenersForEventType'
という実行時エラーになります。

また、作成された widget の widget.js に、
exports['addEventListener'] = $.label['addEventListener'];
を追加した上で(label は、widget 内で追加されたラベルなどの view の id です)、
$.mywidget.addEventListener( EVENTNAME, FUNCTION);
してみます。これは、iOS では正しく働くようですが、Android の場合、
05-04 11:45:58.802: E/V8Exception(4776): Exception occurred at ti:/events.js:172: Uncaught TypeError: Object #<Controller> has no method '_hasListenersForEventType'
という実行時エラーになります。



多分バグです。

ということで、いろいろ思案したあげく、現状の Alloy (1.1.0とか、1.1.1) では、
$.mywidget.label.addEventListener( EVENTNAME, FUNCTION);
のような書き方をせざるをえないようです。
(widget 内の view がカプセル化されていないのは、なんだか解せない気もしないでもないですが ... )

widget にも on が使えるようになってるのが一番いいんですけどねぇ。

プロジェクト一式

widgeteventtest







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の値が、具体的にどうなるのかは、手探りの状態だ。


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

2013/03/28

Android TextToSpeechの初期化について

Android TextToSpeech API の初期化についていろいろ調べてみると、Googleの発表直後のサンプルをベースにした説明が多く、今ひとつ ACTION_CHECK_TTS_DATA の存在意義がわからず、試行錯誤したり、Android のソースをあさったりしてみた。結果、ある程度満足のいく方法を見つけたので報告します。

典型的な説明

典型的な TTS のインスタンスの初期化手順の説明では、

1)ACTION_CHECK_TTS_DATA 投げる(非同期)

Intent intent = new Intent();
startActivityForResult(intent, REQ_CHECK_TTS);

2)1)の結果を onActivitiyResult() で受ける

protected void onActivitiyResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQ_CHECK_TTS) {
  if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
    ......
  }
}

3)2)を受けて TTS エンジンのインスタンスを作成する(非同期)
tts = new TextToSpeech(this, this);

4)3)の結果を onInit() で受ける

public void onInit(int status) {
  if (status == TextToSpeech.SUCCESS) {
   // TTSオブジェクト初期化成功 ...
  }
}

5)言語を設定し、実際の発声を行う


Locale locale = 適当なロケール;
if (tts.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE) {
  tts.setLanguage(locale);
  tts.speak(text , TextToSpeech.QUEUE_FLUSH, null);
}

のようになる。

2)で、resultCode が  CHECK_VOICE_DATA_PASS になっている場合に、「TTSエンジンが使えるようになった」と説明されるのが普通であるが、定数の名前がそうはなっていないので説明と齟齬があるように感じる。しかも、端末購入直後の Google TTS エンジンしか入っていない状況なら、選択できる TTS エンジンは1つだけなので、ユーザーには何の選択肢も与えられないが、日本のユーザーが TTS を使う場合、大抵  KDDI Lab の N2 TTS エンジンを入れるだろう。その状況で、1)を行うと、 Google TTS か N2 TTS かを選択するダイアログがでて、選択することになる。しかし、N2 TTS を選択したとしても、端末設定の TTS の設定値が Google TTS になっていれば、3)の時点で生成される TTS エンジンのインスタンスは Google TTS になってしまう。ユーザーの選択が反映されるのは、ただただ、2)の時点でかえってくるインテントに含まれるTextToSpeech.Engine.EXTRA_AVAILABLE_VOICES で取り出し利用可能な言語情報だけだ。この値の中から適当なものを選んで、5)の段階で isLanguageAvailable () せずに、無理矢理 setLanguage() しても、TTS エンジンが違うので、偶然一致するものがない限り、反映されない。

欧米圏以外で、自国語の TTS エンジンを利用しようとすると、以上のような状況になってしまって、UX 的に非常にまずいことになる。
たぶん Google の説明が不十分なのだ。そして、不十分な情報を元に英語だけ発話させておしまいのサンプル説明がはびこっているのだ。
それにもまして、API の構成があまりよろしくないと思う。よろしくないので、直してほしいが、多分どうかならないので、自前でなんとかしなければならない。ここからが本題です。

TTS エンジンは切り替え可能

まず問題なのは、TTS エンジンの切り替えだ。これができないと、いちいちユーザーに設定画面から、切り替えてから使ってもらわざるを得ない。
古い API では、

tts = new TextToSpeech(this, this);
tts.setEngineByPackageName(enginepackangename);
のように setEngineByPackageName() で、エンジンのパッケージ名を使って切り替え可能だ。
API Level 14 以降はこれが廃止されて、

tts = new TextToSpeech(this, this, enginepackangename);
のように、コンストラクタの引数としてパッケージ名を与えて、所望の TTS エンジンを生成できる。多分途中で切り替えると何かと問題が起こったので、こういう仕様になったのであろうと推測する。また、どの API Level でも
tts = new TextToSpeech(this, this);
は、端末設定の設定値のよってTTSエンジンが決まる。

TTS エンジンのパッケージ名も取得可能

これについては、公開APIの中にそれらしいものが見当たらない。しかし以下のようにすれば、自前で可能だ。
PackageManager pm = getPackageManager();
Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE);
List<ResolveInfo> resolveInfos 
     = pm.queryIntentServices(intent,PackageManager.MATCH_DEFAULT_ONLY);
if (resolveInfos != null) {
  for (ResolveInfo resolveInfo : resolveInfos) {
    ServiceInfo service =  resolveInfos.serviceInfo;
    if (service != null) {
     // ここでTTSエンジンの情報を集める
     // service.packageName の値がTTSエンジンのパッケージ名
    }
  }
}


ACTION_CHECK_TTS_DATA の本当の役割

実のところ、ACTION_CHECK_TTS_DATA をしないでも TTS エンジンの生成は可能だ。全く問題なく機能する。端末設定に従って発話するだけなら問題ない(が TTS エンジンの辞書にない文字列が来たら、TTS エンジンはシカトすることになる)
TTS エンジンを生成する前に、TTS エンジンのパッケージ名から、サポートするロケールの一覧がとれればいいことになる。当然 Android OS 内部ではそれができているので、方法さえわかれば、後はまねするだけだ。で、発見したのが、以下のコードだ。

intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
intent.setPackage(enginepackangename);
startActivityForResult(intent, REQ_CHECK_TTS); 
インテントに、setPackage() でパッケージ名を指定してやるだけで、例の TTS エンジン選択ダイアログは出ずに、欲しいエンジンのロケールだけを返してくれる。
一緒に、パッケージ名もつけてかえってきてくれると何かと便利なんだが、そうなっていないので、与えたパッケージ名は自前て管理しないといけないのがちと面倒くさいが、我慢しよう。アイディアとしては REQ_CHECK_TTS は任意の整数値が使えるので、これをうまいこと使うてもあるかと思う。
で、ACTION_CHECK_TTS_DATA の本当の意義は、EXTRA_AVAILABLE_VOICES で使えるロケールを知らせるばかりでなく、EXTRA_UNAVAILABLE_VOICES で使えないものも同時に教えてくれる点にある。「使えないものって無限にあるじゃないか」と怪訝に思うだろうが、ACTION_CHECK_TTS_DATAのインテントに EXTRA_CHECK_VOICE_DATA_FOR として、チェックしたいロケールをつめてやると、使えるもの/使えないものに分類してくれる。これが、ACTION_CHECK_TTS_DATAの元々の意味だった訳だ。
使えるロケールの一覧が欲しいだけなら、EXTRA_CHECK_VOICE_DATA_FORは設定しないでかまわない。

以上をふまえれば、より良いUXのTTS アプリが作成できるでしょう。


*注意
N2 TTSで、setLanguage()を実行しても、指定のロケール(女性/男性)に切り替えできません。多分N2 TTS の仕様だと思います。
もしかかすると、ロケール情報に女性/男性の識別に含まれてしまっているのが、実装者を混乱させているのかもしれません。
*注意
SVOX Classic TTSというTTSエンジンが無料でダウンロードできるのだが、インストールすると、SVOX Classic TTSのパッケージ名とともにACTION_CHECK_TTS_DATA を投げたとき、Google TTSとSVOX Classic TTSの選択ダイアログがでてきてしまう。そしてSVOX Classic TTSを選択すると、SVOX Classic TTSのでもアプリが起動されてしまうので、本来欲しかった情報が得られないことになる。無料配布の宣伝用のエンジンなので致し方ない気もするが、UXを破壊していることに変わりはないとおもう。