Androidのアプリ開発関連のブログ

エディタ系コマンドで2日ハマった

制作中のSSHクライアントで、vi(VIM)とlessコマンドで下端と上端でのカーソル移動での新行表示が重なっちゃってハマってた。

下端と上端で原因は違うのだが、
下端の方はLFの際に最大行数を超える場合に行を挿入する処理で、行番号は0からで行数は1からなのに間違って比較してたのが原因だったぽい・・・
これで偉いハマった・・・

上端の方はCSIコマンドへの対応が必要なので、下端の方でハマっててこっちは後回しになってた。

下端と上端の件でハマって細かくログ出力したのだが、

vi(VIM)はスクロールが発生する際に"CSI[n;mr"で一時的に最下段をスクロール対象から外して、
上方向移動の際はスクロール範囲を戻してから1行目にそのまま文字を出力。
下方向移動の際は最下段が外れた状態でLFで行挿入をして文字入力。そして範囲を戻す。
ってな流れになってるぽい。
上方向移動は一旦範囲を縮小して、戻してそのまま文字が入力されてるぽいんで、
「範囲縮小 → 範囲拡大」となった場合は、上に行を挿入するぽい?

lessコマンドでは、スクロール範囲は設定されず、下方向への文字挿入は常に最下段に行われる。
なので、下方向への移動はただ文字が送られてくるだけ。
上方向に移動する際は「ESC+M」コマンドが送られてて、これは1行上にスクロールする制御コマンドぽい。


というわけで、多分理解できた。
モノの方はだいぶ出来てきた感じ。
マルチバイト文字とかはどうすっかな・・・

TextViewの高さ

制作中のSSHクライアントで画面に横方向と縦方向に何文字表示できるか測っているのだが、
レイアウトの構造は、「 ScrollView > LinearLayout > TextView 」で1行毎にTextViewが入る。
LinearLayout はmatch_parentしても高さがないようなので、全体の高さはScrollView.getHeight()で取得。
TextView.getLineHeight()で文字の高さを取得して全体の高さを割って行数を取得していた。
paddingとmarginは指定無し。

で、なんかズレているような・・・
というわけで計算値の出力と画像の解析をしたのだが、
TextView.getLineHeight()は13になるのだが、画像解析では15px程のような・・・

さらに調査してみたが、LinearLayout.getHeight()は15pxになる!
LinearLayoutに考慮してない余白か何かがあるのかと思ったが発見できず、
TextViewの方は挿入してないのでgetHeight()は0になる状態だったのだが、挿入済みのTextViewを取得してgetHeight()してみたら15になった!

TextViewの高さが予定と違っていることが確定したが、
getLineSpacingExtra() は0。
getLineSpacingMultiplier() は1.0。
getLineHeight() は13なので、Viewの高さも13になるものと思っていたのだが、謎の余白が入るんだね。
試しにpaddingとmarginを0pxに指定してやってもViewの高さは変わらず15。


というわけで、この問題は挿入済みのTextViewからgetHeight()して高さを取得することにする。

SSHクライアントを制作中

引き続きSSHクライアントを制作中。
入出力だけの機能になるのでUIとか簡単と思ってたが、制御コードの実装とかめんどい。

カーソル位置の制御とかは、サーバーからASCIIコード27の'ESC'に続けて91の'['、さらにパラメータが続いて最後に英大文字。
っていうコードがきて、それに従って制御する感じぽい。
Control Sequence Introducer(CSI)っていうものらしい。
不完全な感じがするが、座標系のコマンドは実装した。
ただ、スクロールで座標ずれてくるからそのへんの対応が必要がありそう。

あと、Ctrlキーで制御するようなコマンドの送信はどうやるんだ?
というわけでハマったが、
ASCIIコードの64'@'から95'_'までの32文字に、Ctrlが押されている場合は64を引いた値がASCIIコード0-31の制御コードに対応してる。
というわけで、対応するASCII制御コードを送れば機能する。
でもって、AndroidのキーコードはASCIIコードとは違うので対応させる必要がある。
範囲中のA-ZはASCIIコードで65-90で、Ctrlで64減算すると1-26になり、Androidのキーコードでは29-54の範囲で連番になってるので、キーコードから28を引けば対応する制御コードになる。
Ctrlが押されている場合はgetUnicodeChar()が0の印字不能文字となる。
その他のキーは連番じゃないので個別に設定する必要がありそうだが、'^'と'_'はキーボードによってShiftが必要だからと思うが定数化されてない。なので、その2つは非対応にした。


まあ、そんなわけで、Ctrl+CとかCtrl+Dなんかが送れるようになった。
あとは、
スクロール系の処理。
スクリーンサイズの送信。(フォントサイズを計算する必要があるね)
色系の処理。
ってな感じかな。

nanoとかviとかエディタコマンドに対応させるのが難易度高い部分と思うが、テスト用のSSH鯖にnanoが入ってなかった。
公開鍵認証に対応させたらnano入りの鯖に繋いで試してみるが、
スクリーンショット取るのに外部鯖だとホスト名とかユーザー名とか隠したい。
手元にテスト鯖用意してやるかな。

キー入力の取得処理でハマってる

今日はキー入力の取得処理でハマって、全く進んでない。

まず、EditTextなしでキーボードを表示してキーイベントを取得する方法で考えた。
AndroidManifest.xml で<activity>の属性にandroid:windowSoftInputMode="stateAlwaysVisible" を追加すればキーボードは常時表示される。
ActivityのonKeyDown(int keyCode, KeyEvent event)
をオーバーライドすれば押されたキーのボタン番号とKeyEventが取得できる。
のだが、ここで戻りがStringとかCharのメソッドを調べて、
「ボタンに対応する文字を取得する機能が存在しない。」と判断してしまった。


次にEditTextなしは諦めてEditTextの変更をTextWatcherで監視する方法を試みた。
普通にEditTextを使うと確定前に表示されちゃうので、TextWatcherで確定判別。
確定時にイベントが発生すれば良いのだが、そういうイベントはない模様。

この場合はソフトキーボードの強制表示は、
InputMethodManager imm=(InputMethodManager)getSystemService(this.INPUT_METHOD_SERVICE);
imm.showSoftInput(view,imm.SHOW_IMPLICIT);
てな感じでできる。
showSoftInput()の第1引数がViewだが、Viewなしでキーボード表示は動的にできないのかな?

で、TextWatcherでの確定判別だが、
public void afterTextChanged(Editable s){
   for(Object obj:s.getSpans(0,s.length(),Object.class)){
     if((s.getSpanFlags(obj)&s.SPAN_COMPOSING)==s.SPAN_COMPOSING){
       Log.d("TES","未確定"+s.toString());
       return;
     }
   }
   Log.d("TES","確定"+s.toString());
}
こんな感じで、Spanned(Editableがimplementsしてる)のフラッグを確認すると、未確定状態だとSPAN_COMPOSINGが付いているようなので、それを判別。
なのだが、キーボードをゆっくり押すと問題ないが、速く押すと未確定のはずが一瞬確定してる。
他に、未確定状態だとアンダーラインがついているはず!ということでSpannedでアンダーラインを判別しても似たようなことが出来るようなのだが、
キーボードを押すと一瞬アンダーラインが消えてるのが目視できる。結局同じぽい。



どうしたもんかと思ったが、
実は最初に試したActivityのonKeyDown()の方で、
戻り値がint型なので気づかなかったが、event.getUnicodeChar()でボタンに対応するchar値(intで返ってくるが)が取得できた。
isPrintingKey()で表示可能文字かどうかの判別もできるんで、
表示可能ならそのまま送信して、表示できない文字は個別に処理作れば良さそう。

というわけで、ActivityのonKeyDown()でキー入力を取得する方法で行こうと思うが、1byteずつ送ることになるんで、
マルチバイト文字の入力が必要な場合とかは入力エリア使ってやるしか無いかな。

SSHクライアントの制作をちょっとやった

SSHクライアントの制作をちょっとやったが、
SFTPの時と同じようにJSCHでセッションをconnectして、
ssh=(ChannelShell)sess.openChannel("shell");
InputStream s2c=ssh.getInputStream();
ssh.connect();
てな感じで、openChannel("shell")でChannelShellを取得。
チャンネルをconnectする前にgetInputStream()で取れるInputStreamを読むことでサーバーからの出力を読み取ることができる感じ。
まだ試していないが、getOutputStream()で取れるOutputStreamから入力ができるはず。
JavaのInputStreamとOutputStreamは、どっちが入力でどっちが出力だかわかりにくい・・・

サーバーからの出力(InputStream)は繋いでる間は切断されないのでread()は-1にならない。
最後に制御コードが来るとかもないようなので、1byteずつ処理するしか無いと思うのでUTF以外の文字コードに対応させようと思ったらめんどそう。
まずUTF-8で作って、他の文字コードは対応できそうだったら考えることにする。

nanoとかtopみたいな画面が切り替わるコマンドだとかは何か制御コードが飛んでくるものだと思うのだが、
今の所ログイン処理までしか作って無くて、そこまででは改行と復帰以外の制御コードは確認できなかった。
次は入力をできるようにしてnanoとかコマンド送ってレスポンスを確認してみようと思う。
nanoなんかはターミナルの画面全体に表示されるが、サイズはどうやって決定してるんだろ?
と思ったが、setPtySize(int col, int row, int wp, int hp)でサイズを指定できるみたいだ。wpとhpはpxぽいが、colとrowは文字数なのかな?

入力はEditTextとかなしでキーボード常時表示にして入力を取得できないか?
という方向で考えている。