概要
Titaniumで既にiOSアプリを開発・リリースしている状況でAndroidアプリも開発することになり、同じ仕様のアプリ(iOSアプリはWebviewを用いた所謂ガワだけアプリ)を開発するにあたってはまったポイントと解決案。
はまったこと
WebviewのUA偽造とセレクトボックス
クライアントがアプリかどうかをサーバサイドで判定する為に、iOSではUAに適当な文字列を付加する実装になっていました。ところがAndroidでも同じように実装したところ、Webview内のセレクトボックスが今開いてるWindowの下の層?に表示されてしまい、選択できないどころか見えもしない問題が発生しました。これについてググると公式のJIRAが起票されていて、Titaniumのバグのようでした。
Android : Titanium WebView.setUserAgent() breaks HTML selection
https://jira.appcelerator.org/browse/TIMOB-17139
とりあえず解決するのを待てる状況ではなかったので、UAをいじらず他の方法でアプリかどうかをサーバサイドで判定できるような実装に変更することで解決しました。
Push通知の受信
GCMを使ってPush通知を受信する為に以下のモジュールを使って実装しました。
iamyellow/gcm.js
https://github.com/iamyellow/gcm.js
http://iamyellow.net/post/40100981563/gcm-appcelerator-titanium-module
上記のブログの記事通りに実装してみたところ、2つの問題が発生しました。
- アプリを開いてない状態で通知をタップするとAlloyやunderscoreなどが見つからないと怒られる
- アプリが未起動時に通知をタップしてもスプラッシュ画面から進まない
あまり英語を読むのに慣れてないんですが上記ブログのコメント欄を根気よく読んでたら2つとも解決することができました。
1つ目
該当コメントはこのあたり
http://iamyellow.net/post/40100981563/gcm-appcelerator-titanium-module#comment-1190468861
私の場合はAlloyとunderscoreをrequireすることで解決しました。
 /*global Ti: true, require: true */
  
 +var Alloy = require('alloy'),
 +    _ = require('alloy/underscore');
 +
  (function (activity, gcm) {
  
      var intent = activity.intent;
2つ目
該当コメントはこのあたり
http://iamyellow.net/post/40100981563/gcm-appcelerator-titanium-module#comment-964813660
tiapp.xmlでanalyticsをfalseにすることで解決しました。
 -    <analytics>true</analytics>
 +    <analytics>false</analytics>
カスタムURLスキーム
Webview内のHTMLとTitanium側で連携を取りたい場合にカスタムURLスキーマを用いました。
例えば以下のようなコードです。
<a href="scheme://hoge=fuga">アプリ側で何かするリンク</a>
アプリ側ではWebviewのbeforeloadイベントにリスナーを登録してe.urlがscheme://で始まるものだったらe.stopLoading()してアプリ側でやりたい処理をする、みたいなコードになってました。
// Webviewのbeforeloadイベントリスナー
function doOnBeforelaod(e) {
    if (e.url.search(/^scheme:\/\//) !== -1) {
        e.stopLoading();
        // アプリ側でやりたい処理
    }
}
こうすることでWebview側の遷移をストップしつつ、アプリ側でやりたい処理をするという意図のようでした。iOSではこれでうまく動いていたのですが、Androidだとe.stopLoading()する前にどうしても scheme://なんてページ存在しないよ つってエラー画面を出してしまいました。
Ti.App.fireEvent()を使うことも考えましたが、調べてみたらTiをグローバルに使えるのはTitaniumのプロジェクト内にあるhtmlだけで、リモートにホストしてるhtmlについては不可能なようだったので諦めました。
(追記:リモートにあるHTML内からTi.App.fireEventを使う方法についてコメントいただきました。)
ということで、WebviewのURLのハッシュが変わるとWebviewのloadイベントが発火することが分かったので、以下のように解決しました。
<a href="scheme://hoge=fuga">アプリ側で何かするリンク</a>
<script>
	// Androidの場合だけ実行される処理
	$(function() {
		$('body').on('click', 'a', function() {
			var href = this.href;
			if (/^scheme/.test(href)) {
				location.hash = 'androidapp=' + href;
				return false;
			}
		});
	});
</script>
// Webviewのonloadイベントリスナー
function doOnLoad(e) {
    // catch hash changed
    var hashIndex = e.source.url.search(/#\/?androidapp/);
    if (hashIndex != -1) {
        setTimeout(function() {
            // アプリ側でやりたい処理
            // hashを削除して画面をロードし直しておく
            setTimeout(function() {
                e.source.setUrl(e.source.url.substr(0, hashIndex));
            }, 500);
        }, 500);
    }
}
画像の配置バス
Androidで各端末毎に最適な画像を使う場合は各解像度にあった画像を用意する必要がありました。
その際にはまったポイントを以下にまとめてます。
http://qiita.com/torufuruya@github/items/640efa040a06732f32fa
Cookieの管理
これが今回のAndroidアプリ開発で最も苦労したポイントでした。
まず前提として以下の2点の場合の話になります。
- サーバサイドでCookieを用いた処理をしてて、かつサーバサイドでCookieを焼く際に domainを指定してない
- Webviewを用いていてWebviewとHTTPClientでCookieを同期する必要がある
これらの条件に当てはまる場合、TitaniumでCookieを焼くと実際は同じドメイン・パス・名前のCookieでも2つ存在してしまうという問題がありました。例えば以下のような感じです。
// Titanium側でCookieをadd
var cookie = Ti.Network.createCookie({
	domain: 'example.com',
	path: '/',
	name: 'test'
});
Ti.Network.addSystemCookies(cookie);
// <<< サーバ通信前
// サーバ側で同じpath/nameのCookieが焼かれたとして...
// サーバ通信後 >>>
var cookies = Ti.Network.getSystemCookies('example.com', '/', 'test');
// cookies[0] => Titanium側でaddしたcookie
// cookies[1] => サーバサイドで焼かれたcookie
// 0-1の順番はどうなるか忘れてしまいましたmm
サーバサイドでdomainを指定した場合は上記のような重複が発生しないことを確認しました。が、セキュリティ的にできればそこは妥協したくなかったのでクライアントサイドだけでなんとか解決できないかといろいろ試行錯誤しました。そして以下のような解決策を取りました。
- Webviewのonloadイベントリスナーで
- document.cookieからいらないやつ消す
- document.cookieから必要なcookieを取り出してTi.App.Propertiesに保管(アプリ再起動時でも最新のCookieを使えるように)
 
- HTTPClientでリクエスト投げる前にTi.App.Propertiesから最新のCookieを取り出してリクエストヘッダに追加
- HTTPClientのonloadイベントリスナーで
- Ti.Network.addSystemCookie(httpCookie)してHTTPCookieとSystemCookieを同期(Cookieをいじるならここも直接document.cookieをいじってしまう方がベターだったかも)
- Ti.App.Propertiesの更新
 
// domain未指定のcookieが最新のものなので、不必要なdomain指定のcookieを消す
var date = new Date();
date.setTime(0);
var pastDate = date.toGMTString();  //Thu, 01 Jan 1970 00:00:00 GMT
$.webview.evalJS('document.cookie="'+name+'=;domain='+domain+';path='+path+';expires='+pastDate+'"');
終わりに
TitaniumはJavascriptでiOS/Android共にアプリを作成できてとても素晴らしいです。が複雑なアプリを実装する場合は最初からiOSとAndroidをワンコードで実現することを諦めた方がよいと思いました。iOSとAndroidの制御がそこら中に発生して可読性も悪くなります(これは設計にもよるかもですが)。iOS/Androidどちらか一方で使えない機能が思ったよりも多いなーと思ったのが正直な感想でした。
また今回タイトルにもありますが、Android初心者(恥ずかしながらAndroidのタスクマネージャーからタスクを消す方法を覚えたのがリリース2日前とかでした)が1ヶ月でリリースまで持っていかなければならず、期間的に厳しいこともあり上述のような強引な対応になってしまいましたが、もっと良い方法があると思いますし、ご指摘いただけたら嬉しいです。
Titanium x Androidの参考文献をググっても2014年代に書かれたエントリをほぼほぼ見なかったので(Qiitaにはたくさんありますね!)Titaniumを使ってる人が減ってるのかもしれませんが、今回のこの投稿がどなたかの参考になれば幸いです。