Androidでの通話状態の補足と状態遷移（実装編）


#概要
[設計編](http://qiita.com/b-wind/items/e42aba55004fcc5f60f4 "Androidでの通話状態の捕捉と状態遷移")では、TelephonyManagerから得られるイベントを利用して「通話状態の状態遷移」を状態遷移表を用いて整理しました。

本記事では、それを元に実際のプログラムコードを作成してみます。
なお使用する開発プラットフォームは Android Studio(Java) ではなく「Xamarin for Android(C#)」です。

★注意★
ここに掲載したプログラムコードは全てサンプルです。
そのまま利用した場合は誤動作が発生する恐れもありますのでご注意願います。

#機能の概略
 * TelephonyManagerからイベントを受け取るブロードキャストレシーバ クラスを作成します。
 * 「現在の電話状態」と「取得イベント」を状態遷移表の上で突き合わせ、処理すべき内容（セル内の記述）を取り出します。
 * 取り出した『処理すべき内容』、C#が提供する「イベントデリゲート」を介してクラス外へ通知します。<br/>また、通知先の処理から返却される「遷移先である次の状態」はクラス内部に保存し、次回の突き合わせに利用します。
 * 未記入のセル（処理すべき内容が無いセル）だった時は、そのイベントを無視して状態遷移も実施しません。


##状態遷移表

|　|アイドル<br/>（状態A）|発呼操作開始<br/>（状態B）|発呼・送話中<br/>（状態C）|着呼中<br/>（状態D）|受話中<br/>（状態E）|
|:-----|:-----|:-----|:-----|:-----|:-----|
|NEW_OUTGOING_CALL<br/>（イベント1）|[発呼操作開始]<br/>→ 状態Bへ| | | | |
|PHONE_STATE(OffHook)<br/>（イベント2）| |[発呼・送話開始]<br/>→ 状態Cへ| |[受話]<br/>→ 状態Eへ| |
|PHONE_STATE(Ringing)<br/>（イベント3）|[着呼開始]<br/>→ 状態Dへ| | | | |
|PHONE_STATE(Idle)<br/>（イベント4）|<center>---</center>| |[送話終了]<br/>→ 状態Aへ|[着呼取消]<br/>→ 状態Aへ|[受話終了]<br/>→ 状態Aへ|
　※ 状態A-イベント4 セルは状態が遷移しないので「無処理」と見なします。

#ブロードキャストレシーバクラスの作成
Android.Content.BroadcastReceiver クラスを継承して新しいレシーバクラスを作成します。

```C#
/// <summary>
/// 電話の発信・着信イベントを受け取るレシーバクラスです。
/// </summary>
public class PhoneStateReceiver : BroadcastReceiver
{
    /// <summary>
    /// このクラスの新しいインスタンスをレシーバとして登録します。
    /// </summary>
    /// <param name="ctx">コンテキスト。</param>
    /// <param name="receiver">レシーバクラスのインスタンス。</param>
    public static void RegistReceiver(Context ctx, TelephoneStateReceiver receiver)
    {
        IntentFilter filter = new IntentFilter ();
        filter.AddAction ("android.intent.action.PHONE_STATE");
        filter.AddAction ("android.intent.action.NEW_OUTGOING_CALL");

        ctx.RegisterReceiver (receiver, filter);
    }

    /// <summary>
    /// このクラスのインスタンスをレシーバから登録解除します。
    /// </summary>
    /// <param name="ctx">コンテキスト。</param>
    /// <param name="receiver">レシーバクラスのインスタンス。</param>
    public static void UnregistReceiver(Context ctx, TelephoneStateReceiver receiver)
    {
        ctx.UnregisterReceiver (receiver);
    }

    /// <summary>
    /// 発信・着信イベントを受信するイベントハンドラです。
    /// </summary>
    /// <param name="context">コンテキスト。</param>
    /// <param name="intent">インテント情報。</param>
    public override void OnReceive (Context context, Intent intent)
    {
    }
}
```

このレシーバは特定のActivityクラスから利用することを想定しています。
レシーバの登録・解除用メソッドもこのクラス自身が提供し、特定の「インテントアクション（ブロードキャストアクション）」を受け取るためのインテントフィルタも、このメソッドの中で用意します。
　※ このクラスが必要な措置は「このクラス自身」が用意します。

#状態遷移表の実装
まず、「状態」と「イベント」を表す定数を用意します。
状態遷移表の横軸と縦軸の要素をそのまま定数化していますが、インテントアクションを受信したものの有効なイベントと判断できなかった事象を示す「Unknown（不明なイベント）」を追加しています。

```C#
/// <summary>
/// 状態を示す定数です。
/// </summary>
public enum CallCtrlStatus
{
    Idle,
    OutGoingCall,
    OutConnect,
    InComingCall,
    InConnect,
}

/// <summary>
/// イベントを示す定数です。
/// </summary>
public enum CallCtrlEvent
{
    Unknown,
    OutGoingCall,
    OffHook,
    Ringing,
    Idle,
}
```

　
二次元配列を利用すれば、状態遷移表のイメージそのままに実装を進めることができます。
ただし次の点には注意が必要です。

 * 状態やイベントが多くなると巨大な二次元配列が必要となるため、コーディング量が増大し「ガベージコレクト不可能なメモリ」の消費量も必要以上に増える。
 * 状態やイベントの追加・削除は「列」や「行」の追加・削除を意味するが、表が巨大になるほど修正ミスが発生しやすい。（行または列単位でコピー＆ペーストを行った後にミスが発生しやすい）
 * 通信プロトコル制御などの速度性能にシビアな処理の場合、ループ処理での配列操作では速度性能が低下する恐れがある。（100ms以上の速度差は人が十分に認識可能です）

上記の事柄を避けるため、ここではイベントと状態の二つを「ネストしたDictionaryオブジェクト」の形で表現することにします。
　※ ハッシュキーによるセル情報への高速アクセスが見込めます。

なお、イベントと状態を突き合わせて得られる結果として「状態遷移表におけるセル位置」を格納しておきます。

```C#
/// <summary>
/// 状態遷移表。
/// </summary>
private static Dictionary<CallCtrlEvent, 
               Dictionary<CallCtrlStatus, string>> _Metrix =
 new Dictionary<CallCtrlEvent, Dictionary<CallCtrlStatus, string>>()
{
    {
        // イベント1
        CallCtrlEvent.OutGoingCall,
        new Dictionary<CallCtrlStatus, string>() {
            // 状態A
            { CallCtrlStatus.Idle, "A1" },
        }
    },
    {
        // イベント2
        CallCtrlEvent.OffHook,
        new Dictionary<CallCtrlStatus, string>() {
            // 状態B
            { CallCtrlStatus.OutGoingCall, "B2" },
            // 状態D
            { CallCtrlStatus.InComingCall, "D2" },
        }
    },
    {
        // イベント3
        CallCtrlEvent.Ringing,
        new Dictionary<CallCtrlStatus, string>() {
            // 状態A
            { CallCtrlStatus.Idle, "A3" },
        }
    },
    {
        // イベント4
        CallCtrlEvent.Idle,
        new Dictionary<CallCtrlStatus, string>() {
            // 状態C
            { CallCtrlStatus.OutConnect, "C4" },
            // 状態D
            { CallCtrlStatus.InComingCall, "D4" },
            // 状態E
            { CallCtrlStatus.InConnect, "E4" },
        }
    },
};
```

　
イベントとの突き合わせに使う「現在の状態」を保存しておくためのメンバ変数も宣言しておきます。

```C#
/// <summary>
/// 現在の状態。
/// </summary>
public CallCtrlStatus _currentStatus = CallCtrlStatus.Idle;
```

#イベント遷移処理の実装
状態遷移表での突き合わせにより得られた結果（セル位置）は、クラスイベントの形でレシーバクラスの外部へ通知します。
.NETのクラスイベント機構は非常に強力で、ごく簡単な記述で複数の宛先へ同じイベントオブジェクトを一斉配信することができます。
必要な定義は次の二つです。

 * イベント配信先メソッドの形式を定義するイベントデリゲート型。
 * イベントデリゲート情報を管理する「event」プロパティ。

　
注）デリゲート型はレシーバクラス定義外に宣言してください。

```C#
/// <summary>
/// イベントデリゲート型。
/// </summary>
/// <param name="cell">
/// イベントと状態との突き合わせ結果。（セル位置を示す文字列）
/// </param>
/// <return>次の遷移先を示す「状態」。</return>
public delegate CallCtrlStatus OnCellMatchedHandler(string cell);
```

　
イベントプロパティを作成し、レシーバクラス利用者が自由に「イベントハンドラ（デリゲートメソッド）」を登録・解除できるようにしておきます。

```C#
/// <summary>
/// セル位置に処理が格納されているときに呼び出されるイベント。
/// </summary>
public event OnCellMatchedHandler OnCallMatched;
```

　
TelephonyManagerからイベントを受け取り、状態遷移を繰り返していくメイン処理は OnReceiveメソッドに記述します。

```C#
/// <summary>
/// 発着信イベントを受信するイベントハンドラです。
/// </summary>
/// <param name="context">コンテキスト。</param>
/// <param name="intent">インテント情報。</param>
public override void OnReceive (Context context, Intent intent)
{
    if (intent == null)
        return;

    // 受信したインテントアクションから「イベント」を取得する。
    CallCtrlEvent evt;
    if (this.GetMetrixEvent (intent, out evt) == false)
        return;

    // 「イベント」と「現在の状態」を元に、状態遷移表 から
    // セル内容を取得する。
    string cell = this.GetMetrixCell (evt);
    if (string.IsNullEmpty(cell) == true)
        return;

    // イベントハンドラの呼び出し。
    if (this.OnCellMatched != null)
        this._currentStatus = this.OnCellMatched (cell);
    }
}
```

##「イベント」の取得（生成）
状態遷移表に記載した「イベント」を生成します。

```C#
/// <summary>
/// 「イベント」を取得します。
/// </summary>
/// <param name="intent">インテント情報。</param>
/// <param name="evt">取得したイベント。</param>
/// <returns>
/// イベントを取得できたときは true。 それ以外のときは false。
/// </returns>
private bool GetMetrixEvent(Intent intent, out CallCtrlEvent evt)
{
    evt = CallCtrlEvent.Unknown;

    if (intent.Action == "android.intent.action.NEW_OUTGOING_CALL") {
        // 「NEW_OUTGOING_CALL」イベント
        evt = CallCtrlEvent.OutGoingCall;

    } else if (intent.Action == "android.intent.action.PHONE_STATE") {
        var state = intent.GetStringExtra(TelephonyManager.ExtraState);

        if (state == TelephonyManager.ExtraStateIdle) {
            // 「PHONE_STATE(Idle)」イベント
            evt = CallCtrlEvent.Idle;

        } else if (state == TelephonyManager.ExtraStateOffhook) {
            // 「PHONE_STATE(OffHook)」イベント
            evt = CallCtrlEvent.OffHook;

        } else if (state == TelephonyManager.ExtraStateRinging) {
            // 「PHONE_STATE(Ringing)」イベント
            evt = CallCtrlEvent.Ringing;
        }
    }

    if (evt != CallCtrlEvent.Unknown)
        return true;

    return false;
}
```

このメソッドでは CallCtrlEvent型の定数値 を「イベント」としていますが、下記のタイミングで「通話相手の電話番号」を取得することもできます。
イベントハンドラ（デリゲートメソッド）を介して電話番号できるよう、周辺ソースを含めて改造してみるのも一興です。

 * NEW_OUTGOING_CALLイベント
 * PHOBE_STATE(Ringing)イベント


##イベントと状態の突き合わせ
ネストされたDictionaryに対し、「イベント」と「現在の状態」が交差する位置のセル値（状態遷移表のセルを指し示す文字列）を取得します。
ここでは単なる文字列を扱っていますが、実際のビジネスシーンではより多くの情報を含有しているコンテナクラスオブジェクトがディクショナリ化されるはずです。

```C#
/// <summary>
/// 「イベント」と「現在の状態」を突き合わせ、両方が交差する場所のセル値を取得します。
/// </summary>
/// <param name="evt">突き合わせる「イベント」。</param>
/// <returns>交差位置のセルに格納されている値。</returns>
private string GetMetrixCell(CallCtrlEvent evt)
{
    if (_Metrix.ContainsKey (evt) == false)
        return null;

    Dictionary<CallCtrlStatus, string> cells = _Metrix [evt];
    if (cells.ContainsKey (this._currentStatus) == false)
        return null;

    return cells [this._currentStatus];
}
```


#最後に
「電話の状態」は Android OSと標準アプリ によって制御されるため、普通のアプリが正確にその状態を知ることは困難です。
しかし発信する電話番号へのプレフィックスの付加や特定相手からの着信を処理契機に用いるなど、それでもなお応用範囲は広がります。

学習ネタとしても面白い分野なので、一度試してみてみることをお勧めします。