ssh経由でgrowlnotify実行

LinuxサーバからMacクライアントのGrowlに通知を出したい

MacBookAir買いました。MacといえばなんとなくGrowlということでなんでもかんでもGrowlで通知したい!Growlと連携するには基本的にはAppleScriptとかいうのを使うらしいのですが全く分かりません…しかし!コマンドラインなどから通知する機能として「growlnotify」というコマンドが用意されています。やったね!
このコマンドをサーバ側から実行すればなんとかなりそう…

サーバ側からコマンドを実行するには

サーバからMacBookAirにssh接続してgrowlnotifyコマンドを実行する方法でやろうと思います。
なんか逆な気もするけど…

MacBookAirのsshd有効化

GUIのシステム環境設定の「共有」にてリモートログインを有効に
・パスワードログイン有効化(/etc/sshd_config に「PasswordAuthentication yes」と記述)

ssh経由でコマンド実行

基本的には「ssh user@host command」を実行してその後パスワードを打てばリモート接続先でコマンドを実行することが出来ます。
user=hoge
host=192.168.0.2
command=/usr/local/bin/growlnotify test -m "こんにちは" -a test
ならば
ssh hoge@192.168.0.2 "/usr/local/bin/growlnotify test -m \"こんにちは\" -a test"
となります。エスケープを忘れずに!

ただ、これだと毎回パスワードを打つ必要があります(sshdでパスワードを必要としない設定にすればまあOKですが…)。メンドクセ

ログインを自動化する

 いろいろ調べてみるとUNIXコマンドの「expect」を使うといけるっぽいです。パスワードがhogehogeの時の具体的なシェルスクリプトは以下のとおりです。

#!/usr/bin/expect
set timeout 10
spawn ssh hoge@192.168.0.2 "/usr/local/bin/growlnotify test -m \"こんにちは\" -a test"
expect "Password:"
send "hogehoge\n"
interact

「expect "Password:"」のところは環境によって変わるかもです。「expect "assword"」とかにしたほうが汎用性があるかも…

でこのスクリプトを動かせば一応Growl通知は出たのですが必ず成功するわけではなく結構失敗します…
原因不明!これじゃなかなか使いづらいな

終わりに

今回はやや不満が残る結果に終わってしまいましたがexpectとか今まで知らなかったので勉強にはなりました。めでたしめでたし

adbで遊ぶ

abc2012sでadbの面白さを知りました

 本日 abc2012 Spring に行って来ました。そこで adb の話を聞いて、とても面白そうだったので少し遊んでみました。とりあえず adb のポートフォワーディング機能を使って、PCで取得したHTMLデータをandroidに送ってWebViewに表示させてみました。

adbの仕組み


↑こんな感じらしいです。普段使っているのは基本的に adb-client で、バックで adb-server が動いているようです。直接使っていなくてもeclipse等でも裏ではadbが動いています。

ポートフォワーディングのやり方

「adb forward tcp:xxxx tcp:yyyy」
と打てばOKです。xxxxはローカルホストで使うポート番号、yyyyはandroid端末で使うポート番号です。
まずandroid端末側でポートyyyyで待ち受けるサーバプログラム的なモノを動かします。その後ローカルホストのxxxxポートに向けてローカルホスト側のクライアントプログラムでsocket接続し、データを流し込むとandroid端末側のポートyyyyへ送られます。おもしれー!
※逆にすると(ローカルホスト側でサーバプログラムを動かそうとすると)、address already in use になります…今回の用途ではこっちのほうが自然なのだが


では作ってみましょう。まずはサーバ側(android側)から。適当なので内容は問わないで…

ProxyWebActivity.java

package xxxx.yyyy;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.webkit.WebView;
import android.widget.EditText;
import android.widget.Toast;

public class ProxyWebActivity extends Activity {
	private WebView wv=null;
	private int serverPort=54321;
	private ServerSocket serverSocket=null;
	private Socket socket=null;
	private PrintWriter pw=null;
	private BufferedReader br=null;
	private int connectedFlag=0;
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        wv=(WebView)this.findViewById(R.id.webView);
        wv.getSettings().setJavaScriptEnabled(true);  

        try{
                //ここに全部書くのはどうかと思うがServerとClientが逆なのでゴリ押し
        	serverSocket=new ServerSocket(serverPort);
        	socket=serverSocket.accept();
        	pw=new PrintWriter(socket.getOutputStream(),true);
    		br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        	Toast.makeText(this,"Connected!!",Toast.LENGTH_SHORT).show();
        	connectedFlag=1;
        }catch(Exception e){
        	Toast.makeText(this,"Connect Failed!! Finish...",Toast.LENGTH_SHORT).show();
        	finish();
        }
    }
    
    public void loadURL(View v){
    	if(connectedFlag==0){
    		Toast.makeText(ProxyWebActivity.this, "Not Connected...", Toast.LENGTH_SHORT).show();
    		return;
    	}
    	try{
    		String url=((EditText)findViewById(R.id.querybox)).getText().toString();
    		if(url==null){
    			Toast.makeText(ProxyWebActivity.this, "No Query...", Toast.LENGTH_SHORT).show();
    			return;
    		}
    		StringBuilder sb=new StringBuilder();
    		pw.println(url);
            String boundary=br.readLine();
            
    		String line="";
    		while ((line=br.readLine())!=null) {
    			if(line.equals(boundary)) break;
    			sb.append(line);
    		}
    		wv.loadDataWithBaseURL("about:blank", sb.toString(), "text/html", "UTF-8",null);
    		//wv.loadData(sb.toString(), "text/html","UTF-8"); ←なぜかこれだとうまくいかない…
    	}catch(Exception e){
    		e.printStackTrace();
    	}
    }
    
    @Override
    protected void onDestroy() {
    	super.onDestroy();
    	try{
    		pw.close();
    		br.close();
    		socket.close();
    		serverSocket.close();
    	}catch(Exception e){
    		e.printStackTrace();
    	}
    }
}

※socketを使うので、必ずpermission.INTERNETが必要です

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
	<LinearLayout android:orientation="horizontal"
    	android:layout_width="fill_parent"
    	android:layout_height="wrap_content"
    	android:gravity="center_vertical"
    	>
    	<EditText android:layout_weight="1"
    	    android:hint="http://"
    	    android:id="@+id/querybox" 
    	    android:layout_width="0dip" 
    	    android:layout_height="wrap_content" />
    	<Button android:text="GO!"
    		android:layout_width="wrap_content"
    		android:layout_height="wrap_content"
    		android:onClick="loadURL" />
    </LinearLayout>
    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

続いてクライアント側。rubyで書きました。

require "socket"
require "open-uri"

HOST="localhost"
PORT=54321

soc=TCPSocket.new(HOST,PORT)

while(true)
  url=soc.gets
  next if(url==nil)
  puts "accept!!"
  boundary=Time.now.to_i.to_s+Time.now.to_i.to_s
  soc.puts(boundary)
  soc.puts(open(url).read)
  soc.puts(boundary)
  puts "send!"
end

手順

1.PCとandroidをUSBケーブルで接続!
2.adb start-server
3.adb forward tcp:54321 tcp:54321 ←適当
4.android側アプリ起動
5.クライアントアプリ起動
6.android側アプリのテキストボックスにURLを入力し、GOボタンを押すと…

結果


androidのデータ通信をOFFにして、テキストボックスに http://www.facebook.com を入力しGOボタンを押すと無事ページが表示されました。
しかし当然ながら画像出てないしUserAgentを偽装していないので「お使いのブラウザには互換性がありません」のメッセージ(笑)
まともにやろうと思えばもう少し作りこみが必要ですがまあ今回はとりあえずページが表示できたのでよしとする!
サーバとクライアントが明らかに逆なのでホントはandroid側でページを取得してクライアント側で表示させる、テザリング的なやり方が自然でしょう。
androidのデータ通信をOFFにしないと勝手にWEBから画像を拾ってくるので要注意

終わりに

今回はUSBケーブルで接続してますが、無線でも可能です。ただしandroid2.3とかだとrootがないと無理っぽいです。
またandroid4.0からはandroid端末でadbを動かすことができるらしいです。ということはandroid端末同士を接続して…めちゃ面白いですな!!
4.0端末欲しいよーー

パケット通信を止める!

パケット通信を止めるには

 パケット通信止める方法としてまず考えられるのがUIMカード(SIMカード)を抜いてしまう方法でしょう。ただわざわざ抜くのもめんどくさいし、他のところにも様々な悪影響を及ぼす可能性があるのであまり実用的ではないでしょう。
 ググってみると一般的にはAPN(アクセスポイントネーム)をわざと変なものに書き換えて通信できなくしてしまう方法がとられているようです。実際androidマーケットにもこの手のアプリが多く存在しています。

やってみる

APNを書き換える方法はググったら一発で見つかりました(Android開発技術録 参考にさせて頂きました!ありがとうございます)

さっそくコピペさせていただいて試してみました。ところが!!我が htcEVO ではAPNの書き換えは出来てもパケット通信が遮断される様子は全くない!どゆこと!? そもそもAPN名が「internet」ってなんだよ(笑)


調べてみるとそもそもauが採用しているCDMA2000という方式ではAPNというものを使ってないらしいことが判明…なんじゃそりゃorz

解決策

じゃどうすればいいのか…いろいろ調べてみるとandroid2.3から非公開APIとして"setMobileDataEnable"というのが存在するらしいです。これをリフレクションで呼び出せば一応パケット通信を止めることが出来るらしい。

試して見ましょう!以下適当メソッド

private toggleMobileDataEnabled(boolean enabled){
    ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
    try{
        Method method=cm.getClass().getMethod("setMobileDataEnabled", new Class[]{Boolean.TYPE});
        method.invoke(cm, enabled);
    }catch(Exception e){
        e.printStackTrace();
    }
}


この手法ならめでたく htcEVO でも動きました!!


そして android2.2搭載のIS05 で試したところ例外をキャッチしたのでやはりandroid2.2以下の端末には通用しないっぽいことも判明…
手動でやれということですか

終わりに

とりあえずパケット通信を遮断するにはAPI書き換え手法が使えそうな端末ならAPN書き換えを、そうでなければ非公開APIの setMobileDataEnable で設定変更を、それもできないauのandroid2.2以下の端末に関してはあきらめろという結論でよろしいでしょうか?


ただandroid2.2以下環境の上で動くパケット通信設定変更するアプリって結構あったような気も…スイッチ系ウィジェットとか。
もうちょっと調べないといけませんな


早速この機能をつかって例のアプリを完成させなければ…

AlarmManager について

AlarmManagerとは

 指定した時間に処理を実行したり、一定間隔で連続して処理を実行出来る仕組みです(cronみたいなもの)。たとえば1時間に一度RSSを取得しにいってウィジェットの情報を更新させたりとかに使えます。

問題点?

しかし!このAlarmManager、当然のごとくスリープ状態でも機能し続けるのであまり短い間隔で処理を呼び出してるとCPUリソースをガンガン消費してバッテリーも食いつぶす恐れがあります…(そもそも基本的にそんなに短い間隔で呼び出す用途に用いるものではないかも)


たとえばAlarmManagerを使って、一定間隔(10秒とか)でスライドショーウィジェットの表示を切り替えたりするような処理を行うとき(非常に限定的w)は、スリープ状態になったら(スクリーンがOFFになったら)処理をキャンセルしたほうがいいです。スクリーンがONになったら再びAlarmManagerをsetすればいい


ただしAppWidgetProvider単独ではスクリーンオフのブロードキャストを受信できないため、Service等を利用する必要があると思われます。


ということで、結局何が言いたいのかよく分からない記事でした!以上!

N2 TTS を試して見る

N2とは?

 KDDI研究所作の日本語音声合成エンジン。超軽量が売りで、ネットにつながなくても使える!こりゃ使うしかねぇ!アプリがしゃべると何かと楽しそうw
インストールから設定については androidマーケット を参照してください。

アプリに組み込む

 アプリに組み込むにはどうするかというと、上記設定後、普通に TextToSpeechクラスを使えばいいっぽい。SetLanguage メソッドは Japanese に設定すべし。あとは setPitch メソッドでピッチを変えて声色を変えます。


 ただ、KDDI研究所作のN2を使ったウィジェット「ささやくヤーツ」のキャラの声を再現するにはピッチ調整だけだとおそらく無理なので、何かしらのパラメータが存在しているのだと思われます。
speak メソッドの引数として何かパラメータを与えられるんでしょうか?誰か教えてー(´Д`)

アプリ作成

 音声合成が出来るということで対話型アプリを作りたいところですがわざわざ返答とか考えるのがメンドクセー…ということでとりあえず、音声認識で聞き取った言葉をキャラがそのまま発するという、結構無意味なものを作ってみるw


何かかわいいキャラが欲しいところですが残念ながら絵心ゼロの私。
ということで maku puppet を使わせていただきす!マクパペットは無料で使えてなんと商用利用もOKなかわゆいキャラクター(flash製)。パラメータを与えるだけで装備を変更したりアニメーションを付けたりを簡単にでき、AS3から簡単に操作できたりする優れもの。
ただしネットにつながってないと使えない…

これを使ったflashandroidのWebViewで表示すればなんとなくいけそうな気がする…
いろいろ頑張ればキリがないと思うけどもとりあえず元のmaku.swfをassetsフォルダに置いてそれにパラメータをつけて毎回ロードするようにすればなんとなくそれっぽく動くはず。ポイントはWebViewの設定でプラグインを有効にすることと(これをしないとflash見れない)スクロールバーを消すことでしょうか。

String makuurl="file:///android_asset/maku.swf";
wv=(WebView)findViewById(R.id.wv);
WebSettings settings = wv.getSettings();
settings.setPluginsEnabled(true);
wv.setScrollBarStyle(WebView.SCROLLBARS_INSIDE_OVERLAY);
wv.loadUrl(makuurl);  


次は音声認識のところですがこれもまあネット見ればゴロゴロとやり方は載ってますな…。ただ、毎回ダイアログを表示するのもなんなのでダイアログを表示しないやり方でやるべき。イベントリスナーを設定するとちゃんと話し始めとか終わりとか検知してくれて、すごいぞ!SpeechRecognizer
後は認識が完了したところでその文字列をTTSに渡して、適当にアニメーションを付けてswfをロードし直せばそれなりに動くはず!


ただ問題として、ひたすら音声認識しっぱなしだと…
ユーザがしゃべる→認識→アプリがしゃべる→アプリ自らしゃべった音声を認識→アプリがしゃべる→認識→アプリがしゃべる→…
という無限ループに陥ってしまうので(笑)アプリがしゃべり終わったあとに認識を開始しないといけない。
TextToSpeechクラスにはしゃべり終わった事を知らせてくれるイベントリスナー(setOnUtteranceCompletedListener)が用意されているのでありがたく使いましょう!
注意点としてあらかじめUTTERANCE_ID(識別するための番号?多分適当で問題なし!)というのをパラメータで与えておかないとイベントを補足できない点と、イベントリスナーをセットするタイミングが早すぎるとこれまたイベントが発生しないらしい点です(onInitメソッド中で定義するのが吉)。あと処理はhandler経由にしないとバグるっぽい?

//CompleteEvent
tts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener() {
    public void onUtteranceCompleted(String utteranceId) {
        handler.post(new Runnable(){
	    public void run(){
                rec.startListening(RecognizerIntent.getVoiceDetailsIntent(getApplicationContext()));
	    }
	});
    }
});
//SET UTTERANCE_ID
ttsparam = new HashMap<String, String>();
ttsparam.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, UTTERANCE_ID);
tts.speak(text, TextToSpeech.QUEUE_FLUSH, ttsparam);

スクリーンショット



ということで簡単ですが作成したアプリ MakuTalk のプロジェクトzipとapkは以下のサイトに…
mukacho's informal android apps!
※当然ながらN2TTSが無いと動きません…

Bluetoothペアリングにトライ!

アプリ内でペアリングしたい!

 androidとPCで同期をとれるアプリを作成中なう。
一応ほぼ実装完了したので細かい部分(ペアリング設定とか)をやりたいなと思いました。.....しかし!ペアリングは思った以上に難解ですよみなさん!(ペアリング以外もBluetooth関係は何かと訳分かりませんが…)


 そもそも近年の?androidではアプリで勝手にペアリングすることはセキュリティ的にあまりよくないと考えられているようで、ペアリング用?のメソッドたちも隠蔽されて基本的に利用出来なくなっているようです(createBondやsetPinなど)。
android developer を見てみると、ペアリングされていないデバイスに接続しようとすると勝手にペアリングが始まるみたいなことが書いてあります。.....が私が試したところ、ただただ接続エラーが出るだけでしたorz 多分android同士だとうまくいくんでしょうな


では他のアプリはどうなっているかというと…
 PCとファイルのやりとりが出来るアプリ「Bluetooth File Transfer」の場合、普通にアプリ内でペアリング出来ました(笑) なぜだー!!?


ググった末たどり着いたのはリフレクション?を使う方法。たとえば以下のような感じ

リフレクションを使えば createBond 等を実現可能

// BluetoothデバイスをMACアドレスから取得
BluetoothDevice device = btAdapter.getRemoteDevice(btMacAddress);

// ペアリング開始処理呼び出し
Method createBond = device.getClass().getMethod("createBond", new Class[] {});
Boolean res = (Boolean)createBond.invoke(device);

リフレクションってすげー!


 ということでリフレクションを使う方法でいろいろ試してみたんですが、ペアリングの手順とか仕組みとかもう分からんわーー! そもそも createBond 呼び出したらどこからともなく通知バーに通知が現れて、まあその通知を使えば一応ペアリング出来たのは出来たんですがなんでそもそも通知なのか、普通にアプリ上にダイアログで表示させてくれないのか とかいろいろ意味不明だったので.....あきらめました!ガッデム!!


ペアリングに関してはユーザに丸投げし、ペアリングしたいときは設定画面を呼び出してもらうようにしました…

Bluetooth設定画面呼び出し方法

Intent intent = new Intent();
intent.setAction(android.provider.Settings.ACTION_BLUETOOTH_SETTINGS);
startActivity(intent);   


あとは自分で勝手にペアリングしてくれと!!
くやしいのー

アラートダイアログ(AlertDialog)の表示位置を変えるには?

表示位置を変えたい!

 アラートダイアログは初期状態だと画面中央に表示されます。まあ別にそれでもいいんですがたとえばダイアログ中にEditTextがあったりするとソフトウェアキーボードが画面下半分に表示される関係で文字入力する際にダイアログが上にずれるんです!
別にずれてもいいじゃーんというならそれまでですが最初から画面上部に表示できたらずれなくていいなと思いましていろいろ…


 いろいろググって試してみた結果以下のコードで表示位置をずらせることを確認!(とっくに知ってるって?)

アラートダイアログを表示するshowDialogメソッド

private void showDialog(){
        AlertDialog.Builder builder;
	AlertDialog alertDialog;
   		
	LayoutInflater inflater=(LayoutInflater)this.getSystemService(LAYOUT_INFLATER_SERVICE);
	View layout=inflater.inflate(R.layout.dialoglayout, (ViewGroup)findViewById(R.id.layoutroot));
   
   	builder=new AlertDialog.Builder(this);
   	builder.setView(layout);
   	alertDialog=builder.create();
   	
        //ココからがポイント
   	WindowManager.LayoutParams wmlp=alertDialog.getWindow().getAttributes();
   	
    wmlp.gravity=Gravity.TOP;    //画面上部に表示
        //wmlp.gravity=Gravity.BOTTOM;  //画面下部に表示
    //wmlp.y=50;           //中心から下方向に50pxずらす
   		
   	alertDialog.getWindow().setAttributes(wmlp);
   	alertDialog.show();
}

 show()メソッドで表示する前にWindowManager.LayoutParamsを設定し直す事により表示位置を制御出来るようですね

結果画像一覧(一応Twitterクライアント想定。画像とかアイコンとかは適当w)

普通 上部表示 下部表示 下に50pxずらす

おわりに

 画面上部に表示することは出来ましたがどうも変な余白があるのが気になる!
一番上に表示されて欲しいな…