様々な大きさの円を重ならないように描く

あるテレビ撮影現場で解像度1920×1080のスクリーンに700個程の円を敷き詰めて描き動かすという要件があり JavaScript の Canvas を使ったのですが、円の座標を取得する際にブラウザの応答がなくなってしまう問題を避けるために注意した点を二つ書き留めておきます。

この画面を避ける

一つ目は重なり判定に Math.sqrtMath.pow を使わないことです。二つの円が重なる条件を言語化すると「中心点からの距離が、半径の合計よりも小さい」となって、直訳すると Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)) < r2 + r1 と表すことができるのですが、三平方の定理を平素に解釈し関数を使わず (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) < (r2 + r1) * (r2 + r1) とした方が処理が速くなります。

参考

二つ目は Web Workers の利用です。表示する円座標を格納しておく配列の数が上限数に到達するまで繰り返す部分 while(circles.length < circlesMax){...} を丸ごと worker.jsself.addEventListener('message', (e) => {...}); に記述しました。座標計算が終わるまで「読み込み中…」を表示をするのは UI としても良いし、処理時間の計測をしておき長い場合に中断しやり直すということも可能になり便利なこと極まりありません。

const worker = new Worker('./worker.js');
 
worker.addEventListener('message', (e) => {
  circles = e.data;
  circles.forEach(function(circle){
    drawCircle.call(this, circle.x, circle.y, circle.r);
  });
});
 
worker.postMessage({
  radiusMax: /*最大半径*/,
  radiusMin: /*最小半径*/,
  circlesMax: /*最大表示円数*/
});

これに関連して注意したのは、動作実機はウェブサーバがなくデスクトップ等に置いたファイルを実行する状況から同一生成元ポリシーによるエラーになってしまうので、Chrome にオプションを付けて起動することを予め伝えておく必要があります。

参考

実際には円同士を線で結んだり円や線の増減アニメーションがあったりで、今回初めて書いた Web Workers は参考したコードにもある Promise との併用がとても効果的になる場面が増えてくるのではないかと思いました。

ICSからTVPIDファイルを作る

先日作成したワールドカップ用のカレンダーファイルを利用して、EyeTV の予約に使う iEPG データを作りました。尚、このソニーが提唱した電子番組表の規格は終了する事になるようですが理由は不明です。

ICS ファイルの読み込みは公開されていたものを拝借して利用しましたが、2点変更を加えました。1つ目は、ics クラスで icsData 値を全て取得する際の explode の区切り文字列が \n になっていたのですが、作成した(というか元にしていた) ICS ファイルの改行コードが CRLF だったので次のようにしました。

$icsDatesMeta [$key] = explode ( "\n", str_replace(["\r\n", "\r"],  "\n", $value) );

また、このクラスから $icsEvents 取得後に TVPID ファイルを作る際に終了時間のタイムゾーンが設定されていなかったので次の1行を追加しました。

$endDt->setTimeZone ( new DateTimezone ( $timeZone ) );

更にこの追加行の直上に、決勝トーナメント以降の対戦は延長戦の可能性を考慮して終了時間に1時間を追加しました。

if($i > 47) {
  $endDt->add(new DateInterval('PT1H'));
}

この PHP ファイルと同階層に iepg というディレクトリを 777 権限で作成して実行すると、ブラウザ上では日付順の日程を表示しつつ、iepg-{n}.tvpi というファイルを64種作成します。装置は EyeTV 250 を UNIDEN 地上デジタルチューナー DTH11 からのコンポジット入力で使用しているので、station 値は固定し EyeTV 側の「録画ソース」には「100 コンポジットビデオ」となるようにしました。

Content-type: application/x-tv-program-info; charset=shift_jis
version: 1
station: コンポジットビデオ
year: 2018
month: 06
date: 15
start: 00:00
end: 02:00
program-title: FIFAWC2018 ロシア vs サウジアラビア - A組 第1節
genre: 1
subgenre: 2

iEPG ファイルの準備ができたら、重複を回避するために EyeTV の既存の予約をスタンバイ状態(チェックを外す)にし、全てのファイルをダウンロードフォルダにドロップすると予選リーグ第3節の重複を警告してきますので、それぞれどちらの対戦名を優先するか決定して読み込みが完了します。

スタンバイ状態にした既存の予約を有効にして行くと、重複した予約の対戦名をダイアログで警告してきますので、それらの予約をスタンバイにしながら最終的にどちらの予約を優先するか調整する事になるかと思います。

最後に今回作成した iEPG ファイルをこちらに置きましたので、PC 録画を予定されている方のお役に立てば幸いです。
WC2018IEPGs.zip

P.S. チューナーの方は面倒臭い…

参考:

照会カレンダーを作る

4年に1度のサッカーの祭典が迫って来たところで恒例のカレンダーファイルを作成し共有したいと思います。技術的な要件としては次の3つになります。

  1. 日本語表記にする
  2. DropBox を使って照会カレンダーの形式で共有する
  3. スマホの2大 OS とデスクトップでの表示に対応する

(技術的な事はどうでも良いのでカレンダーファイルを入手したい方はこちらからどうぞ。)

  1. 日本語表記にする

    まず ICS ファイルを作成するのですが、英語やロシア語などの表記を使ったカレンダーファイルは既に幾つか存在しているのでそれを利用します。今回は Fixture が提供しているものを利用しました。一旦カレンダーアプリに読み込んだ後、ファイルメニュー>書き出す>書き出すを選び、書き出した ICS ファイルをテキストエディタで開き、日本語に書き換えます。国名やスタジアム名、対戦ラウンド名やグループ節などは一括置換してグループ組名のアルファベットを追加しました。

    更新:詳細にスタジアムの所在地名と放送テレビ局名を追加し、決勝トーナメント以降の「未定」となっていた対戦国名に試合 ID を利用した表記にしました。

    参考:
  2. DropBox を使って照会カレンダーの形式で共有する

    決勝トーナメント以降の未定となっている対戦国名を更新するためにダウンロード形式では無く照会カレンダーとして共有します。コストを掛けない保存先として DropBox を選択しましたが、ここで一つ罠がありました。

    DropBox でファイルを共有した時にデフォルトで付与される dl 引数は 0 なのですが、これを 1 にすることによって解決しました。

    http://hkitago.tumblr.com/post/173940229306

    また DropBox の共有リンクアドレスはファイル名が変わらない限り普遍だと言う事で、更新が必要な照会カレンダーファイルを配置する条件に合致しています。

    参考:
  3. スマホの2大 OS とデスクトップでの表示に対応する

    具体的には、Apple の MacOS と iOS のカレンダーアプリ、Google カレンダーのウェブアプリ版と Android のカレンダーアプリになり、デスクトップとスマホで同期できるようにします。実はここにも一つトリックがあって、アドレス文字列をクリップボードにコピーした後にカレンダーアプリのコマンドを使って照会(追加)する場合と、ハイパーリンクを使ってカレンダーアプリを起動し照会(追加)する場合で方法が異なります。参考まで、Apple では「照会 (Subscribe)」、 Google では「追加 (Add by URL)」)と言うコマンド名称になっています。

    最初に、アドレスをクリップボードにコピーしてカレンダーアプリから直接追加する場合、MacOS/iOS 共に https://webcal:// のどちらのスキームを使う事が許されていますが、Google カレンダーでは webcal:// のみ使う事ができます。https:// のアドレスを使うと一見読み込みに成功したように見えますが内容を表示する事ができません。また、iOS で webcal:// を使うと SSL オプションが外れた状態になるので気になる方は https:// を使うと良いと思います(MacOS のカレンダーアプリには SSL オプションがありません謎)。

    次に、ウェブ上にあるハイパーリンクを踏んでカレンダーアプリを起動して照会(追加)する場合、MacOS/iOS では href="https://… とすると ICS ファイルがダウンロードされ照会カレンダーの扱いになりませんので href="webcal://… を使う必要があります。この場合も先述した通り SSL オプションはオフのままなのでご注意ください。そして Google カレンダーへ遷移し追加する場合は、href="https://www.google.com/calendar/render?cid=webcal://… と記述する必要があります。

    参考:

と言う訳で長々と書きましたが、リンク先はこちらになります。

Apple 用
FIFA World Cup 2018 照会カレンダー日本語版
Google 用
FIFA World Cup 2018 照会カレンダー日本語版

それぞれのカレンダーアプリから直接照会(追加)する場合は、このアドレスをコピーしてお使いください。

最後に注意点としては、iOS で照会すると MacOS へ同期する事ができないので両端末で同期する場合は MacOS で照会し場所を iCloud にする事と、Android のカレンダーアプリはデフォルトで同期がオフになっている事があります。それでは楽しいW杯ライフを!

参考:

Passive Event Listenerを使う

シングルページアプリケーション(SPA)や最近話題のプログレッシブウェブアップス(日本語ではアプリ)をやっていると利用者にスクロールさせたくない場合、イベントリスナーを使った JS コードので対応ができていたのだけど、iPhone で思うようにならなくて調べると、こんなバグ報告がありました。

ところがもう少し調べるとまたもや Stack Overflow に助けられ、公式文書によると iOS 11.3 と macOS 10.13.4 に搭載されている Safari 11.1 からパッシブモードを使う事が必須になったそうです。

Android は未だ昔のやり方で動いているのですが時間の問題かと思います。

参考に記した日本語文献は2016年のもので、そう言えば微かに当時言われていた記憶が…としても唐突過ぎてバグ報告したくなる気持ちも察しつつ、過去のプロダクトを含めて見直しを啓蒙したいところで書きました。

参考:

iOSのOpenCV環境構築

簡単だった Android に比べて癖が強かった iOS の OpenCV 環境構築について、Xcode 9 と Swift 4 で且つ CocoaPods を使わないという条件下で3つのハマった事を記録しておきます。

  1. 使用する OpenCV のバージョン

    まずはやっぱり最新バージョンと思って OpenCV 3.4 を使うも “Apple Mach-O Linker Error” でビルドに失敗したので Stack Overflow のお世話になると 3.1 以下を使うべしという事。実際には最後に判明した事だけど重要度から最初の項目にしました。

    参考:
  2. フレームワークとラッパーファイルを配置する場所と Create Bridging Header プロンプトが表示されない問題

    この辺りのチュートリアルを説明している日本語文献は多いのだけど、フレームワークとラッパーファイルをプロジェクト直下では無くてグループのフォルダ内に置くと openCVwrapper.mm ファイルのヘッダで #import "opencv2/opencv.hpp" とすると 'opencv2/opencv.hpp' file not found とエラーを出力します。

    結論からすると、ターゲットの Build Settings にある Framework Search Path を正しく指定すればどこに置いて良いという事ですが、プロジェクトフォルダの第一階層に置くのが理想的かと思います。

    http://hkitago.tumblr.com/post/172359880316

    もしグループフォルダに置いた場合、右手にあるプルダウンから再帰(recursive)を選べば通るようになります。

    この模索中にフレームワークの追加と削除を繰り返す事になったのですが、2回目にフレームワークを追加しても Bridging-Header.h を自動的に作成するダイアログが表示されない問題にぶつかりました。フレームワークと3つのラッパーファイルを削除したらターゲットの Build Settings にある Bridging Header を削除する事で解決します。参考の Stack Overflow にスクショ付きで解説があって助かりました。

    参考:
  3. ビルド時の Documentation Issue 警告

    最後は深刻ではないのですが、ビルドすると大量の警告を出力してくるという気持ち悪さ回避で、プロジェクトの Build Settings にある Documentation Comments を NO にしました。

    参考:

最後にサンプルコードを Github で漁っていたのですが、 Swift 3 のコードが多く、しかも Swift 4 でカメラ周りの API の変更点が多い事や、Android との兼ね合いを考慮してできるだけ OpenCV のコードで Swift を使わないものを探してみると、Twitter 検索で面白いものを見つけました。(Google 検索には引っかからなかったのがとても現代的)

Swift 4 用に若干変更を加え、先に Android で作っていたコードを流用して簡単に実装できました。

環境構築1日、実装5分という情けないお話でした。

URLからツイートIDを取得する正規表現

10年ほど前にあったデリシャスというブックマーク共有サービスから移行して使っているタンブラーの運用について、以前は投稿の連携機能を利用して名言などをツイッターへ流していましたが、そのままだと先頭から140文字を可読性の悪い状態で出力することに起因する手動の編集作業が面倒臭くなったことや次のような理由もあって、タンブラーへ入ってくる興味のあるツイッター投稿に対して「スキ」フラグをつけて保存し、その一覧を参照して定期的にリツイートするようにしました。

これはタンブラーとツイッターの API を併用するいわゆるボットと呼ばれるもの(ライブラリは TwitterOAuth と、それにインスパイアして作られた techslides-tumblroauth を使用)で、その中で最も厄介だった部分を考えてみるというのが今回のお題目になり、まずツイッターの URL 形式の種類について調べました。

http://hkitago.tumblr.com/post/164275112841

この条件から次のようなもので利用してみたところ、いくつか取りこぼしが見つかりました。
#https?://twitter.com/?(?:\#!/)?(?:\w+)/status(?:es)?/(\d+)#i

そのログを元に更に調べてみると、この4つに加えてサブドメインの付与 twitter.commobile.twitter.com について考慮する必要があるということが分かり、現状で次の正規表現で運用しています。
#(?:https?://)?(?:mobile.)?(?:www.)?(?:twitter.com/)?(?:\#!/)?(?:\w+)/status(?:es)?/(\d+)#i

preg_match 関数で $match に入れたものを var_dump して確認すると次のように取得できていることが分かります。

array(2) {
[0]=>
string(50) "https://twitter.com/{AccountID}/status/{TweetID}"
[1]=>
string(18) "{TweetID}"
}

これで良し、と思ったところでまた一つ取りこぼしの問題を見つけました。それは to.co で知られる短縮 URL の場合で、運良く Stack Overflow に URL を変換する関数を紹介していた方がいらしたので、タンブラー投稿 JSON データの “caption”、”summary” と “body” キーの文字列値に含まれる短縮 URL を取得して返すように調整して使うことにしました。

function getFollowUrl($string) {
  preg_match('#https?://t.co/[a-zA-Z0-9\-\.]{10}#i', $string, $match);
  if(!$match) {
    return $string;
  }
  $url = $match[0];
  $ch = curl_init();
  curl_setopt_array($ch, array(
    CURLOPT_URL => $url,
    CURLOPT_HEADER => false,
    CURLOPT_NOBODY => true,
    CURLOPT_FOLLOWLOCATION => true,
  ));
  curl_exec($ch);
  $follow_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
  curl_close($ch);
  return $follow_url;
}

ここで発生した問題は正規表現の g フラグの使い方です。この関数の引数 $string にはテキストエリア入力による文章が入ってくるので複数の短縮 URL を持っていることがあります。$follow_url を配列で返せば良いかと思うのですが、この場合(極力避けたい)連投ツイートになってしまうことが必至で、最近ツイッターに実装されたスレッド機能による関連内容の表示にも期待しながら一旦処理を保留することにしました。

最後に余談で、URLを含む際の正規表現はバックスラッシュ地獄を避けるのが吉だと思いました。

http://hkitago.tumblr.com/post/164816686035

処理全体の説明は別の投稿にしたいと思いますが、タンブラーに流れてくる投稿のスキボタンを押すだけでツイッターの運用も可能になるのは大変楽なこと極まりないです。

参考:

滑らかな要素移動

画面の上から降りてくる通知ダイアログ部品のように、シングルページアプリケーションではDOM 要素の移動アニメーションを利用することが多く、近年の環境では CSS アニメーション、下位互換を考えた場合には jQuery といったライブラリを使うことかと思います。

http://hkitago.tumblr.com/post/166998101130


ページスクロールの参考コードを応用しバニラやピュアと呼ばれる手法で書き流用できるようにしました。

var moveTo = function(element, to, duration, speed, direction) {
  var rect = element.getBoundingClientRect()
  ,   difference = to - (direction === false ? rect.top : rect.left)
  ,   perTick = difference / duration * speed;
  setTimeout(function() {
    if(direction === false) {
      element.style.top = rect.top + perTick + 'px';
      if(parseFloat(element.style.top) === to) return;
    } else {
      element.style.left = rect.left + perTick + 'px';
      if(parseFloat(element.style.left) === to) return;
    }
    moveTo.call(this, element, to, duration - 10, speed, direction);
  }, 10);
};

検証

<div id="div"></div>
<div id="step"></div>
<style>
body { margin:0; padding:0; }
#div { position:absolute; z-index:0; width:100px; height:100px; background-color:yellow; border-radius:50%; }
#step { position:fixed; z-index:1; width:300px; height:300px; border-right:1px solid lightgray; border-bottom:1px solid lightgray; }
</style>
<script>
setTimeout(function(){
  moveTo.call(this, document.getElementById('div'), 300, 1000, 20, 1);
}, 1000);
</script>

余談ですが、アプリにライブラリを使うと著作権表示の実装が面倒ということもあってこちらを再確認。

参考

Googleマップのルート検索結果をカレンダーに追加するサービスメニュー

iOS 版の Google Maps アプリは電車やバスを使う場合のルート検索結果の詳細表示画面で、再下方にスクロールすると「カレンダーに追加」ボタンがありカレンダーアプリに予定を追加することができますが、MacOS のブラウザを使った場合に同じことができないものかと模索した結果、Automator でサービスメニューに追加するべくワークフローを作成することにしました。ファイルは Dropbox に置いたのでご自由にお使いください。

GMapルート検索結果をカレンダーに追加.workflow

使い方は、

  1. ダウンロード後、ZIP 解凍してダブルクリックをするとサービスインストーラーのダイアログが表示されるのでインストールボタンを押してください。
  2. ルート検索の詳細を表示した画面で、サービス>GMapルート検索結果をカレンダーに追加をメニューから選択してください。現在、Safari と Chrome のみ対応しています。

    電車/バスルート検索の詳細画面以外でサービスを実行するとエラーを返します。

  3. カレンダーアプリが前面に来て一覧を表示するので、予定を入れるカレンダー名を選択して OK を押してください。
  4. 予定の内容は、可能な限り iOS 版に近いものにしました。日跨ぎの予定に対応し、出発/到着時間の指定により一週間内で日付を判定して追加するようにしました。

出発/到着時間の設定にカレンダーで選択ができるにも関わらずその指定日へ予定が追加できないのは、グーグルが電車やバスの運行状況を曜日毎でのみデータを保持しているからなのか、検索結果に曜日しか表示されないという理由があります。AppleScript のソースコードは最初の手順で Automator で開くと見ることができるので、改変などご自由にどうぞ。

技術的な余談で言うと、Yosemite から利用できる JavaScript for Automation (JXA) での記述も検討したのですが、2つのブラウザの処理の違いとコード量、結局 JavaScript で DOM 操作をすることなどを考えると従来の方法になってしまいました。

参考:

AppleScriptでHTML解析

いよいよスプラトゥーン2の発売が迫り子供のお強請りが正に言葉通り凄まじくなって来たので、と言うのが今回の要件です。

実際にはこの初版運用から4回の販売があり、初めの3回は iPhone のメール通知の発見に遅れたためプッシュ通知が確実なメッセージアプリの利用に変更しました。4回目は通知が来たものの5分間隔では時既に遅しと言うことで、巡回の間隔を短くしました。

解析というほど大袈裟ではなく単に変更を確認するのは “SOLD OUT” ボタンタグの有無にし、混雑時のエラーページを回避する条件分岐を加えたそんなこんなで現在この様なコードで常駐しています。

property INTVAL_CHECK : 90
property INTVAL_NOTIFY : 300
property INTVAL : INTVAL_CHECK
property BASE_URL : "https://store.nintendo.co.jp/customize.html"
on idle
 try
  set rawHTML to do shell script "curl -si " & BASE_URL
  if paragraph 1 of rawHTML is "HTTP/1.1 200 OK" and rawHTML contains "カートに進む" then
   set INTVAL to INTVAL_NOTIFY
   my sendMessage("+8180********", "マイニンテンドーストアで Nintendo Switch の販売が開始されました。" & " " & BASE_URL)
   else
    set INTVAL to INTVAL_CHECK
  end if
 end try
 return INTVAL_CHECK
end idle
on sendMessage(targetBuddyPhone, targetMessage)
 tell application "Messages"
  set targetService to 1st service whose service type = iMessage
  set targetBuddy to buddy targetBuddyPhone of targetService
  send targetMessage to targetBuddy
 end tell
end sendMessage

当初 curl コマンドをオプション指定無しに利用していたところ、プログレスメータをアラートダイアログに表示するということがあり s オプションを、更に鯖落ちしていた場合に備え i オプションを付与しレスポンスヘッダが 200 の場合にのみ処理を実行する様にしました。また、メッセージアプリの宛先がメールアドレスにした場合のエラーを解決できなかったので仕方なく電話番号にしました。

自分からメッセージが送られるという特殊性を活かし、iOS のアドレス帳編集からメッセージ着信音を特別分かりやすいものにすると認知向上に尚良いかと思います。(因みにバルーンファイトのゲームオーバー音を設定しました)

最後に開発環境上で動作させることを考慮し Info.plist に <key>LSBackgroundOnly</key><true/> を追加することでドックにもアプリ切替時にもアイコンを表示させない様にしました。後は増産を待つばかり。

追記:
「0以外の状況でコマンドが終了しました。」というエラー内容(英語では “The command exited with a non-zero status”)は、シェルスクリプトの実行が失敗した際に 0 以外の数字を返した場合に発生することとその回避方法をアップルの公式文書で知りました。

http://hkitago.tumblr.com/post/162968916786

man curl を参照すると 1 から 99 までと XX を返す様で、シェルスクリプトのリダイレクトを利用すると空の値を返した際に次の行にある分岐条件が増えることが格好良く思わなかったので今回は Try ハンドラを利用しました。余談で公式文書にツッコミを入れると、リダイレクトを利用する場合は文末にアンパサンドが必要でないか、と…

追記1:
販売が開始されたら通知の間隔を大きくする目的で、2つの巡回用の数値変数を与えて条件分岐で変更できる様にしました。最初の通知を見逃すと痛手なのですが、気づいて買い物手続き中に通知だらけになるのも避けたいという難しいところ、リマインダアプリの「実行済み」ボタンの様な機能を通知持たせることができれば最善だと感じる苦肉の策。

追記2:
「文字列 “SOLD OUT” を含まない」と言う条件だとコード内でコメントアウトされていたり CSS で非表示になっていた場合に失敗することが分かったので修正。

参考:

WebViewのフォーム入力でImmersiveモードを維持する

前回、Javascript の alertconfirm 関数を使って WebView から AlertDialog を呼び出した際に全画面表示を止めてしまう問題について取り組んでいた時に、入力フィールドへフォーカスした際に Immersive モードが解除されるのは仕方がないとしても、その扱いについて考えていたところ、こちらも一癖ある挙動だったので紹介します。

通常 Immersive モードではない場合、WebView にキーボードが被るという問題を解決すべく、AndroidManifest.xml の activity ノード属性として android:windowSoftInputMode="adjustResize" と書くのですが、Immersive モードの場合はこの方法がうまくいきません。そこで試してみたのが、入力フィールドがフォーカスした時に Immersive モードを解除する、という方法です。

まず、MainActivity クラスの中に次のような JavaScript インターフェースを用意し、

public class WebViewJavaScriptInterface{
  private Context context;
  public WebViewJavaScriptInterface(Context context){
    this.context = context;
  }
  @JavascriptInterface
  public void disableFullScreen() {
    getWindow().getDecorView().setSystemUiVisibility(0);
  }
}

WebView について色々と書いてある onCreate の中で JavaScript から呼び出せるように設定しました。

webView.addJavascriptInterface(new WebViewJavaScriptInterface(this), "app");

そしてテキスト入力フィールドを1つ持っている HTML ではこのように JavaScript から Immersive モードを解除するようにしました。

document.querySelector('input[type=text]').ontouchend = function(){
  app.disableFullScreen();
};

ところが、Logcat に “Uncaught Error: Java exception was raised during method invocation” というエラーを出力しうまく動かなかったので、MainActivity で Handler を呼び出し、JavaScript インターフェース内の関数を次のようにすると期待通りの動作をエミュレータで確認しました。

@JavascriptInterface
public void disableFullScreen() {
  handler.postDelayed(new Runnable() {
    @Override
    public void run() {
      getWindow().getDecorView().setSystemUiVisibility(0);
    }
  }, 0);
}

また、JavaScript インターフェース内にもう一つ次のようなものを用意して(前回不要だと思った enableImmersiveMode 関数を再度用意)、

@JavascriptInterface
public void enableFullScreen() {
  handler.postDelayed(new Runnable() {
    @Override
    public void run() {
      enableImmersiveMode();
    }
  }, 0);
}

送信ボタンを押した時に Immersive モードとなるようにしました。

document.querySelector('#submit').ontouchend = function(){
  app.enableFullScreen();
};

実際にこのような挙動で動くようになったのですが、

ソフトウェアキーボード以外の場所で画面を押すと入力フィールドからフォーカスが外れてキーボードが隠れるという OS の設計上、このように加えておくと更に安心です。

document.querySelector('input[type=text]').onblur = function(){
  app.disableFullScreen();
};

実機で検証してみると、処理能力が劣る古い機種ではフルスクリーンになる際にステータスバーが途中で止まってしまったりということが確認できたので、postDelayed で遅延を750〜1000と与えると(動作的にはギクシャクする感は否めないのですが)解決しました。そして、最初に書いていた android:windowSoftInputMode="adjustResize" を外してみると…なんと入力フィールドが隠れずにキーボードに合わせて移動するという世にも奇妙な…。

参考: