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

JSch使うように修正中

SFTPの実装にganymed-ssh-2を使っていたが、接続先鯖によってはパケットサイズが異常で例外が発生しちゃって回避も無理ぽいんで、JSch使うように修正中。

JSchでうまくいきそうなんだが、
以前から判明していたが、自ビルドしようとしても配布物に何か不足がある様でビルドできない。
バイナリも配布されてるんで
バイナリ使えば問題ないが。

使い方は
ganymed-ssh-2以上に難解な感じ。

標準でホストのチェックを行うようになってるが、KnownHostsの設定はファイルパスを渡すようになってて、直接登録はできなそう。
JSch jsch=new JSch();
Hashtable config=new Hashtable();
config.put("StrictHostKeyChecking","no");
jsch.setConfig(config);
てな感じで、
JSchのインスタンスを作成した後に、
"StrictHostKeyChecking"="no" の入ったHashtableをsetConfigに渡すとチェックを回避できる。

公開鍵認証のキーは接続前にaddIdentityで登録しなければならないが、

addIdentityには引数の違うメソッドが複数あるが、ほとんどがキーファイルを渡すメソッドになっていて、
public void addIdentity(String name, byte[]prvkey, byte[]pubkey, byte[] passphrase)
のみが唯一キーを変数で渡せる。
秘密鍵の内容を
byte[]で渡せば公開鍵認証ができる。公開鍵とパスフレーズはnullでいける。
nameは適当な文字列でいいぽい。

公開鍵認証の場合は
jsch.addIdentityで鍵登録だが、
パスワード認証の場合は
Sessionを取得後ににSession.setPasswordで設定と、公開鍵認証とパスワード認証で認証のタイミングが違う。

今のところ
ganymed-ssh-2のように致命的なバグには遭遇してなく、公開鍵認証もパスワード認証もできそうだが、
ganymed-ssh-2と比べて認証までの速度が遅い感じで、数秒かかってる。

ChannelSftpにはcdコマンドがあったり、

ChannelからSessionが取得できて、Sessionを切断するとChannelも全切断してくれるようで、
接続さえしちゃえば
JSchの方が使いやすそうな感じ。
ganymed-ssh-2のSFTPv3Clientにはcdメソッドがないし、SFTPv3ClientからConnectionが取得できないので切断処理もめんどかった。

ganymed-ssh-2ダメだ・・・

SFTPの実装にganymed-ssh-2使ってきたが、
TransportConnection.javaの268行目の条件にあたっちゃってPacketFormatExceptionが発生する場合がある。
SSH鯖は公開鍵認証用とパスワード認証用の2つで試してるが、公開鍵認証用に使ってる鯖で高確率でこの症状になる。
呼び出し元の方でbyte[]の大きさを100万倍に変えて試してみたけど、intの大きさ超えちゃった?うまく行かなかった。
そもそも受信メッセージがそんなに大きいと思わないし、packet_lengthの取得が間違ってるような・・・

鯖によっては問題生じないと思うが、再度ganymed-ssh-2じゃなくてJschの方試してみる。

ganymed-ssh-2使いにくい!

制作中のテキストエディタは、先日FTPSは実装してあったんだが、SFTPの実装をやった。

のだが、予定していたganymed-ssh-2がすごく使いにくい!

接続に関しては、ログイン方法が何通りかあるが、
ssh=new Connection(host);
ssh.connect();
ってな感じで接続して、
パスワードログインなら、
ssh.authenticateWithPassword(user,pass);
こんな感じで、
公開鍵認証なら、
ssh.authenticateWithPublicKey(user,privatekey,null);
ってな感じ。
privatekeyはFileを渡す方法もあるが、変数で直接渡す場合はchar[]型で渡す。3番目の引数は、鍵が暗号化されている場合の復号パス。

接続とログインはそんな感じで難しくないんだが、
まずcd相当のメソッドがSFTPClient(SFTPv3Client)に用意されてない。
SFTPClientにcdがなくてもコマンドを送る方法はあると思うが、それもめんどそう・・・

cdはなくても、必要なリクエスト送る際にフルパス渡しちゃえばまあ問題ないんだが、
getとput相当のメソッドが、
Apache CommonsはInputStreamとOutputStreamを取得して読み書きする仕様だが、
ganymed-ssh-2は、読み込みなら、
SFTPv3FileHandle fh=sftp.openFileRO(path);
書き込みなら、
SFTPv3FileHandle h=sftp.createFile(
path);
って感じでファイルハンドルを取得して、(openFileRWもあるが、ファイルが存在しない時は例外になる模様。createFileなら存在しても平気
sftp.read()とsftp.write()で読み書きするんだが、
readもwriteもbyte[]を渡して読み書きする仕様になってる。
しかも、一度に32768byteしか処理できないのでループ処理する必要がある。
sftp.write()は一度に最大32768byteだが、ループ処理の最後にbyte[]の長さ超える際に、ピッタリ合わせないとダメみたい。

SFTPだけのアプリならそこに処理書いちゃえばいいが、ローカルファイルやFTPにも対応させて文字コードや改行コードの処理も含めて処理を共通化してるが、
byte[]にしなきゃいけないんで、
get処理はsftp.read()で取得できるbyte[]をStringBufferに格納してStringBufferInputStreamにして共通処理へ。
put処理はByteArrayOutputStreamに対して共通処理で書き込んでからbyte[]に変換してsftp.write()で実際に書き込み。
ってな感じで効率悪そうなコードになっちゃった。

そこらの仕様気に入らなかったんで、
ganymed-ssh-2じゃなくてjschを使ってみようかと思ったんだが、
jschは配布ファイルに必要物が含まれていないようで、「create lib-project」してビルドできなかった。
jschはバイナリファイルも配信されてるから自ビルドしないで使うこともできるが、
接続方法からして難解で良くわからんかったので
結局ganymed-ssh-2で実装した。
jschのSFTP系メソッドはcdも実装されてるし、getもputもInputStreamとOutputStream取得できそうな感じではあった。



制作物の方は、かなり時間かかったが、機能的にはほぼ完成な感じ。
当初はHTML編集特化の編集モードを搭載させるつもりだったが、メインはPCで編集して、
モバイルでちょっといじりたい時用で考えてるんで、テキストを編集可能な機能があれば十分と判断したので、実装しないことにした。
自分でも、PCでもHTML専用エディタとか使ったこと無いしね。
画面回転時の復元処理なんかがまだだな。
今週末には配信開始したい。

ここ数日ブログ更新してなかったが・・・

ここ数日ブログ更新してなかったが、引き続きテキストエディタ制作を連日やってる。

可能ならテキストの強調表示機能を実装させようと思ってたが、
TextViewの一部だけを色を変えたりとかできるのか?と思ってたが、
TextViewのsetText()やgetText()で扱うCharSequenceは、実はandroid.text.Editable型?
android.text.Editableは、android.text.Spannableをimplementsしてるんだが、Spannableってので文字列中の範囲に対して装飾を設定することができる。
で、TextViewにはこれに従って文字列中の一部を装飾する機能が備わってるみたい。

TextView.getText()はCharSequenceの参照が取得できるだけだから、SpannableやEditableに型変換して、setSpan()すると一部の文字色などが変えれる。


android.text.Spannable.setSpanはAPIドキュメントだと、
setSpan(Object what, int start, int end, int flags)
パット見じゃイミフだが、

startとendは対象Spannable中の範囲開始位置と終了位置。
Javaの文字列範囲系メソッドはみんなそうみたいだが、終了位置はインデックス+1になる。

Object whatはandroid.text.style以下にあるクラスのインスタンスになる。
基本的にはandroid.text.style.CharacterStyleを継承したクラスになると思うが、文字の色を変えたいなら、android.text.style.ForegroundColorSpan。
setSpanに渡す際は、同一のObjectを複数回setSpanすると最後の一個しか有効にならないぽいんで、
setSpan毎にnewでインスタンスを作成するか、
Object.clone()で複製すればいいと思うんだが、JavaのObject.clone()はクラス側で対応させないとダメなんだね。

int flagsは、Spannableがimplementsしてるandroid.text.Spannedの定数。
いろいろ定義されてるんだが、SPAN_EXCLUSIVE_EXCLUSIVEしか使わなかった。
SPAN_EXCLUSIVE_EXCLUSIVEは、例えば全文字列と適用範囲が「あいうえお」だとして、文字が変化した場合に「あかいきうえくお」といった感じで、範囲外には拡張しないが内部は拡張できる感じになる。
これと逆な感じなのがSPAN_INCLUSIVE_INCLUSIVEで「あかいきうえくお」こんな感じになる。


設定済みspanの変化を監視して変化時の処理ができるなら、
SPAN_EXCLUSIVE_EXCLUSIVEとSPAN_INCLUSIVE_INCLUSIVEを交互に隙間なく並べれば変化時の処理で変化範囲の取得が楽かと思ったんだが、
android.text.SpanWatcherでspanの変化を監視できそうな気がするが、使い方がわからなかった・・・
変化時の処理はandroid.text.TextWatcherでやった。

TextWatcherは、
public void afterTextChanged(Editable s){}
public void beforeTextChanged(CharSequence s, int start, int count, int after){}
public void onTextChanged(CharSequence s, int start, int before, int count){}
の3つのメソッドの実装が必要なinterfaceだが、
TextViewのaddTextChangedListener()でリスナーとして登録できる。
TextView.addTextChangedListener(new TextWatcher(){
  public void afterTextChanged(Editable s){
    変化後の処理
  }
  public void beforeTextChanged(CharSequence s, int start, int count, int after){
    変化前の処理
  }
  public void onTextChanged(CharSequence s, int start, int before, int count){
    変化時の処理
  }
});
って感じで、EditTextの文字列が変化した時に処理できる。
afterTextChangedが文字入力確定後でonTextChangedだと確定してない時点で飛んでくると思うんで、処理はafterTextChangedで行ったが、
afterTextChangedだと範囲が取得できないので面倒な感じ。
Editable sやCharSequence sは対象TextViewに表示されている文字列の参照なんで、操作すればTextViewに表示されている文字列も変化する。

今回はテキストを解析して一部を強調表示するわけだが、文字列が変化するたびに全体を解析しなおしてたら遅くなりすぎるから、
変化範囲を取得して最低限の範囲を解析するように作った。


まあ、そんなわけで強調表示機能は実装した。
TextViewの一部を装飾できたとしても、オープンソースのライブラリかなんか使って多数の言語に対応させなきゃならんかと思ってたが、
独自実装の単一解析処理で言語不問で強調表示するようにした。
当然ながら、全言語同一の処理じゃ良い感じに強調表示できない場合もあるが、わりとうまく行ったと思う。
気づいたのは、JavaとかJavaScriptなんかは記号が少なくてうまく強調表示させやすいんだが、Perlなんかは記号類が多くて難しい。
HTMLもうまく強調表示できる感じになったが、CSSが含まれるとダメかな。内包JavaScriptはうまいこといくようにした。

末尾改行問題に対応した

BufferedReader.readline()だと末尾が改行か違うかがわからない件で対応コード書いた。

読み込み部分
String lf="\n";
BufferedReader buf=new BufferedReader(new InputStreamReader(in,charset));
StringBuffer sb=new StringBuffer();
int c;
while((c=buf.read())!=-1){
   if(c==10){
     sb.append("\n");
     while((c=buf.read())!=-1){
       sb.append((char)c);
     }
   break;
   }else if(c==13){
    sb.append("\n");
    if((c=buf.read())==10){
       lf="\r\n";
    }else{
      lf="\r";
      if(c!=-1) sb.append((char)c);
    }
    while((c=buf.read())!=-1){
      if(c==10) continue;
      if(c==13){
        sb.append("\n");
      }else{
        sb.append((char)c);
      }
    }
    break;
  }
  sb.append((char)c);
}
buf.close();
結局readline()せずに1文字ずつ読むことにした。
改行コードの取得が必要なしょりなんだが、最初の改行で判別する。
元々LFかCRLFの2種類にだけ対応させてたが、ついでなんでCRのみにも対応させた。
CRLFとCRのループ処理は分けたほうがループ内処理が1行減らせるが、基本はLFでCRLFとCR滅多に無いと思うんで共通処理にした。
最初にLFが出現したら基本的にコード内にCRは無いはずなので、全部StringBufferにそのまま格納。
最初にCRが出現したら取得文字列にはLFを格納して、つづく文字でCRかCRLFかを判別。
以降はLFは無視してCR→LF変換。

書き込み部分
OutputStreamWriter writer=new OutputStreamWriter(out,charset);
BufferedReader buf=new BufferedReader(new StringReader(str));
String line;
if(str.endsWith("\n")){
  while((line=buf.readLine())!=null){
    line+=lf;
    writer.write(line,0,line.length());
  }
}else{
  if((line=buf.readLine())!=null){
    writer.write(line,0,line.length());
    while((line=buf.readLine())!=null){
      line=lf+line;
      writer.write(line,0,line.length());
    }
  }
}
buf.close();
writer.close();
読み込み時に変換しているので元文字列にはCRは無い。
元文字列の末尾文字を判別して、末尾が改行かそうでないかで処理を分けてreadline()使う。
末尾が改行なら普通に1行読んで改行コードを追加。
末尾が改行でない場合は、1行目を書き込んで、2行目以降は行末ではなく先頭に改行を追加する感じ。

条件によって処理が2通りになるからコード量増えちゃうが、正確に処理できたほうが良いよね。