2013年1月23日水曜日

不在着信リダイレクタ


不在着信リダイレクタ


Android app on Google Play

概要

Android 端末の不在着信履歴を、 HTTP 経由で別の端末に通知するためのアプリです。

端末も回線も安くなり、特にいろいろな機種がある Android だと、複数回線・複数持ちだったりする場合も多いのではないでしょうか。そういうとき、メールならアカウントを共有できますが、番号だけは共有できないので、不在着信のログだけはいつもの端末に送れると便利です。

この手のアプリは既にメール経由の送信や SMS 経由の送信を実現した物があるのですが、これは HTTP 経由の送信を行うための物です。任意の URL に GET/POST 送信が可能なので、そこから自分のスクリプトで適当な処理を行わせることが可能です。

そこまで凝ったことをするつもりはなくて、単に他の端末に通知したいという目的のために、 Notify My Android の API を叩く機能も実装されているので、通知を受け取りたい側の端末に Notify My Android を入れておけば、 Android 端末上部に現れる通知として不在着信データを受け取ることが可能になります。

今後の目標

  • 通知方式をいろいろ増やす。 SMS やメールもどうせなら対応してしまおう。
  • 通知の送信に失敗したときの、再送信間隔が調整できない問題を修正する。

2013年1月22日火曜日

Cursor.moveToFirst は別に必須ではない

Android の SQLiteDatabase なんかを使用してデータベースに問い合わせを行い、その結果を Cursor で受け取る、という処理は毎日の習慣のような感じでいつも使うわけですが、何故かこれに対して、次のような Cursor.moveToFirst してから Cursor.moveToNext するコードがよく見られます。

Cursor cursor = db.query(...);
if (cursor.moveToFirst()) {
    do {
        cursor.getString(...);
        ...
    } while (cursor.moveToNext());
}

多分最初のカーソル位置がどこにあるか良く分からないとか、名前的にまあ First に移動してから Next に移動するのが正しいんだろう、という感覚で書いてると思うんですが、実際は Cursor.getPosition のドキュメントに次のような記載があります。

When the row set is first returned the cursor will be at positon -1, which is before the first row. 
行セットが最初に返されたとき、カーソルの位置は -1, つまり最初の行の一つ前に設定されています。
つまり、 Cursor インスタンスを受け取ってから最初の Cursor.moveToNext 呼び出しで先頭行を取り出せることはちゃんと仕様上保証されているので、次のように単純なループで書いて大丈夫です。

Cursor cursor = db.query(...);
while (cursor.moveToNext()) {
    cursor.getString(...);
    ...
}

何となくコピペプログラミングで感染しまくってる感があったので、ただそれだけですがメモ。

2013年1月21日月曜日

<intent-filter> に設定する android:priority 属性の意味について

普通はあまり使わないからでしょうか、 前回の記事 がらみで、 AndroidManifest.xml に指定する <intent-filter> の "android:priority" 属性について調べていたのですが、あまりちゃんとした情報がなかったのでメモ。

<intent-filter> は指定した Activity, Service, Broadcast Receiver がどのような Intent を受け取ることが出来るかどうか、という指定で、ここにアクションの種類を識別する文字列や、対応するファイル形式を指定するわけですが、これには "android:priority", その名の通り優先順位を設定することが可能です。

一応、 公式のドキュメント では次のように説明されています。


android:priority
The priority that should be given to the parent component with regard to handling intents of the type described by the filter. This attribute has meaning for both activities and broadcast receivers:
  • It provides information about how able an activity is to respond to an intent that matches the filter, relative to other activities that could also respond to the intent. When an intent could be handled by multiple activities with different priorities, Android will consider only those with higher priority values as potential targets for the intent.
  • It controls the order in which broadcast receivers are executed to receive broadcast messages. Those with higher priority values are called before those with lower values. (The order applies only to synchronous messages; it's ignored for asynchronous messages.)
Use this attribute only if you really need to impose a specific order in which the broadcasts are received, or want to force Android to prefer one activity over others.
The value must be an integer, such as "100". Higher numbers have a higher priority.

android:priority
このフィルタに記述された種類のインテントを処理する場合の、親コンポーネントの優先順位。 この属性は Activity と Broadcast Receiver の両方に意味があります。
  • この優先順位は、このフィルタに合致するインテントに対して、複数の異なる Activity が処理可能な場合、ある Activity が他の Activity に対して相対的にどれだけ優位かを示しています。 Android は数字が大きければ大きいほど優先順位が高いものと見なします。
  • この優先順位は、ブロードキャストメッセージを受け取る際に、 Broadcast Receiver が起動される順序を操作します。 高い数値のものほど、低い数値のものより先に呼び出されることになります。 (この順序は同期したメッセージにのみ適用され、非同期なメッセージについては無視される)
この属性はあなたが本当に特定の実行順序を Broadcast Receiver に強制したい場合、または Android が特定の Activity を他の Activity より優先することを強制したい場合のみに使用してください。
値は "100" のような整数値で、 大きい値ほど高い優先順位を持ちます。
かなり簡単にさらーっと書かれているんですが、三つほど大事なことが抜け落ちています。一つが最大値、一つが Service に対する挙動、一つが Activity に対する制限です。

優先順位の最大値

これはまあ、仕様上設定されていると言うだけのことで、違反したから何か罰があるわけでもなさそうですが、一応 android:priority 値には最大値が存在します。

それが IntentFilter.SYSTEM_HIGH_PRIORITY という定数で設定されており、具体的な値は "1000" です。
最大値と言うよりは、一般的なアプリケーションよりシステムが先に Intent を受け取りたいとき、この "1000" という値を使用するから、一般的なアプリケーションは 1000 未満の値を使用してくれ、というのがより正確でしょうか。従って、通常の最大値は "999" ということになります。

Service に対する挙動

前掲のドキュメントでは Activity 及び Broadcast Receiver についてしか書かれていませんでしたが、 <service> に <intent-filter> が書けるのと同様、もちろん Service に対しても android:priority 値は効果を持ちます。

そもそも Service 自体を暗黙的インテント (クラスを指定しないインテント) で呼び出す場合が少ないからなのかもしれませんが、このときその暗黙的インテントに合致する Service が複数有った場合、 startService や bindService はより優先順位の高い Service を選択するようになります。

Activity に対する制限

で、コレが一番問題かもしれませんが、ドキュメントでは Activity に設定すれば優先順位を上げて他の Activity より優位を強制できる!と書いてあるにもかかわらず、通常のアプリでは Activity に対して android:priority を指定する意味が殆どありません。なぜなら、システム ROM に書き込まれたシステムアプリでない場合、 0 より大きい android:priority 値を設定できないからです。

この事実は前掲のドキュメントでは何も教えてくれなくて寂しいのですが、 Android のソースコードを見てみるとすぐに分かります。 PackageManagerService.addActivity の部分 に、次のようなコードがあるからです。

                 if (!systemApp && intent.getPriority() > 0 && "activity".equals(type)) {
                     intent.setPriority(0);
                     Log.w(TAG, "Package " + a.info.applicationInfo.packageName + " has activity "
                             + a.className + " with priority > 0, forcing to 0");
                 }

ごらんの通り、もしその Activity がシステムアプリ (ApplicationInfo.FLAG_SYSTEM を持つアプリ)のものでなく、優先順位が 0 より大きく設定されていたら、強制的に 0 にしてしまっています。試しに適当なアプリを作って試せば、
"Package test.app has activity test.app.MainActivity with priority > 0, forcing to 0"
というような Logcat 警告を残して、結局 android:priority 値は無視されてしまうことが分かります。

この制限は、多分セキュリティ上のうんたらかんたら、といった理由で課せられているのでしょうけれども、おかげでユーザの操作によらずに飛び交う暗黙的インテントは、基本的にシステムの Google 謹製アプリが受け取ってしまい、自家製アプリでごにょごにょすることが不可能ということになります。

そういうわけで、基本的には sendOrderedBroadcast による Broadcast を受け取る際の優先順位か、暗黙的インテントによる Service 起動の優先順位でしか android:priority の意味はありません。
(Broadcast Receiver, Service については FLAG_SYSTEM 制限はありません)

昨日の話との関連

以上で終わりですが、昨日の話との関連。

自前で ROM をビルドすればどうとでもできるんでしょうけど、どうせなら一般のアプリで Bluetooth HFP のメモリダイヤル機能をなんとかインチキできないかなーと考えていたら、 Bluetooth ヘッドセットの処理は Phone.apk に実装されていて Service として公開されているんですね。で、システムの Bluetooth サービスが "android.bluetooth.IBluetoothHeadset" という暗黙的インテントでこれを呼び出す実装になってるわけです。

ということは、同じ <intent-filter> でより優先順位の高い BluetoothHeadsetService を実装すれば、もしかしたらごまかせるのかもなーと思って調べてました。

一応、 BluetoothHeadsetService を乗っ取ることは可能なようですが、それ以上はちょっと時間が無くて調べてはおりません。

2013年1月20日日曜日

Android 端末では Bluetooth HFP のメモリダイヤル機能が動作しない

Bluetooth HFP (Hands-free Profile) には、仕様上「メモリダイヤル」と呼ばれる機能が用意されています。これは "1 番目" にダイヤルしようとした場合、 1 番目に登録されている電話番号が自動的に呼び出される、いわゆる「短縮ダイヤル」とか「スピードダイヤル」とかいう機能なのですが、これは現状 Android 端末ではサポートされていないようです。

その辺の情報があまりにも少なかったので、ちょこっとメモしておきます。
# まあ、この機能を使う人も少ない、ってことなのかもしれませんが…

Bluetooth HFP はヘッドセットから電話を受け付けたり、あるいはかけたりするためのプロファイルで、例えば端末をポケットに入れたまま、ヘッドセットのボタンを押すなりして電話を受け付けたりすることができます。メモリダイヤルは、具体的にはボイスダイヤル機能で "1 番目にダイヤルしろ" といった音声を認識するような操作で発動し、登録済みの番号へと自動でかけてくれる便利な奴なのです。

この機能は一般のガラケーではほぼサポートされていて、スマートフォンでも iPhone であれば、明示的に「メモリダイヤル番号の登録」といった形ではサポートされていないものの、「連絡先」の「よく使う項目」に登録しておけば、実は一番上が "1 番目"、次の項目が "2 番目"... という形で解釈されて、ちゃんと Bluetooth HFP のメモリダイヤル機能を使うことが出来るように実装されています。

ところが、 Android では何番目を指定しようとも、必ず最後にかけた番号にリダイヤルしてしまうのです。

何でリダイヤルするんだろうなぁ、と不思議に思っていましたが、 Android 本体のソースコードを見れば何のことはない、確かにリダイヤルする仕様になっていました。

Bluetooth HFP ではヘッドセットからのコマンドは全て AT コマンドで送られるようになっており、これはメモリダイヤルであれば "ATD>1" といったコマンドが送られてきたとき、 1 番目の登録先に電話するような簡単な仕様です。その部分のパーサが Phone.app の com.android.phone.BluetoothHandsfree というクラスに実装されていて、 問題の箇所 を見てみると…

         parser.register('D', new AtCommandHandler() {
             @Override
             public AtCommandResult handleBasicCommand(String args) {
                 if (args.length() > 0) {
                     if (args.charAt(0) == '>') {
                         // Yuck - memory dialling requested.
                         // Just dial last number for now
                         if (args.startsWith(">9999")) {   // for PTS test
                             return new AtCommandResult(AtCommandResult.ERROR);
                         }
                         return redial();
                     } else {
                         // Send terminateScoUsingVirtualVoiceCall
                         terminateScoUsingVirtualVoiceCall();
                         // Remove trailing ';'
                         if (args.charAt(args.length() - 1) == ';') {
                             args = args.substring(0, args.length() - 1);
                         }
 
                         args = PhoneNumberUtils.convertPreDial(args);
 
                         Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
                                 Uri.fromParts(Constants.SCHEME_TEL, args, null));
                         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                         mContext.startActivity(intent);
 
                         expectCallStart();
                         return new AtCommandResult(AtCommandResult.UNSOLICITED);  // send nothing
                     }
                 }
                 return new AtCommandResult(AtCommandResult.ERROR);
             }
         });

とまあごらんの有様。 "ATD" のあとに ">" が来た場合、 redial() を呼び出しているのが分かります。 "Yuck - memory dialling requested. Just dial last number for now" (げっ、メモリダイアルが要求されやがった。とりあえず最後の番号にかけよっと…) とまでコメントが付いており、まるで対応するつもりが無いことが分かります。

一応ここで例示したのは Android 4.0.3 (Ice Cream Sandwich) のソースなのですが、このメモリダイヤル部分のコードは Android 1.0 の頃からさっぱり変化無く、現状最新の Android 4.2 Jellybean でも基本的に同じでした。何だか寂しいので自分でコミットしてでも対応させたいんですが、これ単なるバグフィックスじゃなくて、場合によっては ContentProvider を追加したり Broadcast Intent を追加したりって話になるし、新米にはちょっと荷が重そうだよなぁ…。

そういうわけで、 Android 端末ではメモリダイヤル機能が動作しません。ただ例外もありまして、 Samsung の Galaxy Nexus SII とかでは、独自の電話帳アプリに「スピードダイヤル」機能が搭載されており、これに登録するとちゃんと HFP のメモリダイヤルが動作するようなベンダ独自のパッチが適用されていました。変なところで偉い子な端末ですね。

ま、そんなわけで、 Android にメモリダイヤル機能は基本的に搭載されていないから期待するな、というお話でした。

ちなみに Bluetooth HFP 1.5 の仕様書はこちら にありまして、 14 ページ目を
見ると、 "Place a call using memory dialing" (メモリダイヤルを使用した電話の発信) は AG (Audio Gateway, 携帯端末側) での実装が M (Must, 必須) となっているので、ある意味 Android 端末は Bluetooth HFP 非対応、非準拠とも言えるのかなぁと思ったり。
# ATD> コマンドをとりあえず解釈すれば OK なのかもしれませんが

このブログについて

はじめまして。

昨年末から始めた Android プログラミングについて、基本的に stackoverflow あたりを参考にして
作業をしているのですが、その過程で忘れてしまいそうなことをいろいろメモしておく
ためにブログを作成しました。

どうせなら調べたことを Wiki かなにかに纏めようかと思いましたが、
それだと性格上さらに細かいところまで調べようとしてしまい、結局メモとして完成しなくなり、
本末転倒な感じになってしまうので、ブログ形式で適当に書くことにしました。

そのため細部は割と誤りが多いかも分かりませんが、その辺はコメントなり
なんなりで適当にご指摘下さい。