再びDialogFragmentとFragmentのコールバックな関係

前回、DialogFragmentとFragmentをコールバックで連携させるコードを書きました。

また、ダイアログ表示中に画面回転させると例外が発生する場合があることを述べました。

そこで今回は、同じような動作で且つ画面回転しても例外が発生しないコードを書いてみます。もちろん、他にもやり方はあると思いますが、アイデアの一つとして捉えてもらえればと思います。

方針としては、DialogFragmentとFragmentを直接的にコールバックで連携させるのではなく、それらの間にコールバックメソッドを貯めるクラスを用意します。仮にそのクラスをCallbackPoolと名づけます。呼び出し元のFragmentからこのCalbackPoolに対してコールバックメソッドを登録。そして、呼び出されたDaialogFragmentからはCallbackPoolにアクセスして登録済みのコールバックメソッドを引き取って実行します。その際は、直接メソッド名でやりとりするのではなく、メソッドとともに登録したコマンドでやりとりします。

つまり、「Fragment→【CallbackPool】←DialogFragment」という形でFragmentとDialogFragmentがなるべく疎に連携するようにします。画面回転してもCallbackPoolはロストされないためアプリケーションが落ちることはありません。

具体的には以下のコードになります。なお、厳密にするにはエラー処理等も書くべきですが、説明として簡素化したいため最小限のコードにしました。

// 呼び出し元のFragment
public class MyFragment2 extends Fragment {

    // コールバックメソッドと同時に登録する16進数コマンド
    private final int ADD_TEXT = 0x01;
    private final int CLEAR_TEXT = 0x02;

    private Button mDispDialog;
    private TextView mSelectedText;
    // コールバックメソッドを登録するための領域
    CallbackPool pool = CallbackPool.getInstance();

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        // コールバックメソッドを登録
        // (呼び出してもらいたいときのコマンドと、登録したいメソッド名と引数の型を登録)
        pool.addMethod(ADD_TEXT, set(this, "addText", Integer.class));        
        pool.addMethod(CLEAR_TEXT, set(this, "clearText"));        
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.my_fragment, container, false);
        mSelectedText = (TextView) v.findViewById(R.id.textSelected);

        mDispDialog = (Button) v.findViewById(R.id.btnDispDialog);
        mDispDialog.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                FragmentManager manager = getActivity().getSupportFragmentManager();
                MyDialog2 dialog = MyDialog2.newInstance();
                dialog.setTargetFragment(MyFragment2.this, 0);
                dialog.show(manager, "MyDialog");
            }
        });
        return v;
    }

    private  CallbackMethod set(Object instance, String methodName, Class...params) {
      CallbackMethod cm = new CallbackMethod();
      cm.set(instance, methodName, params);
      return cm;
    }

    // 登録対象のメソッド1つめ
    public void addText(Integer selectedId) {
        String text = "none";
        switch (selectedId) {
            case R.id.radioDog:
                text = "いぬ";
                break;
            case R.id.radioMonkey:
                text = "猿";
                break;
            case R.id.radioPheasant:
                text = "キジ";
                break;
            default:
        }
        mSelectedText.setText(text.toString());
    }

    // 登録対象のメソッド2つめ
    public void clearText() {
        mSelectedText.getEditableText().clear();
    }
}
// Fragmentから生成されるダイアログ
public class MyDialog2 extends DialogFragment {

    private final int ADD_TEXT = 0x01;
    private final int CLEAR_TEXT = 0x02;

    // 選択されたラジオボタンのID
    int mCheckedId;
    // 呼び出し元のテキストエリアをクリアするボタン
    Button mClear;

    public static MyDialog2 newInstance() {
        return new MyDialog2();
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        LayoutInflater inflater = getActivity().getLayoutInflater();
        View view = inflater.inflate(R.layout.my_dialog, null, false);
        RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroupOptions);

        radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                mCheckedId = checkedId;
            }
        });

        mClear = (Button) view.findViewById(R.id.btnClear);
        mClear.setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View v) {
                // 呼び出し元のテキストエリアを更新
                // → CLEAR_TEXT(0x02)で登録されているコールバックメソッドを実行
                CallbackPool pool = CallbackPool.getInstance();
                pool.callMethod(CLEAR_TEXT);                                
            }
        });
                
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setTitle("MY DIALOG");
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                // 呼び出し元のテキストエリアを更新
                // → ADD_TEXT(0x01)で登録されているコールバックメソッドを実行
                CallbackPool pool = CallbackPool.getInstance();
                pool.callMethod(ADD_TEXT, mCheckedId);
            }
        });
        builder.setNegativeButton("Cancel", null);
        builder.setView(view);
        return builder.create();
    }
// コールバックメソッド単体を格納するクラス
public class CallbackMethod {
    private Method method;
    private Object instance;
    public Class[] params;

    // メソッド登録
    public void set(Object instance, String methodName, Class... params) {
        this.instance = instance;
        this.params = params;
        try {
            this.method = instance.getClass().getMethod(methodName, params);
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public Object invoke(Integer... params){
      if (instance == null || method == null) return null;

      // --- TODO ---
      // 厳密にはここでparamがmethodに対応した正しいパラメータになっているか、
      // チェックする
      
      try {
        //メソッドを呼び出して実行
        return method.invoke(instance, params);
      } catch (IllegalArgumentException e) {
        e.printStackTrace();
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      } catch (InvocationTargetException e) {
        e.printStackTrace();
      }
      return null;
    }    
}
// コールバックメソッドをいくつも貯めるクラス
public class CallbackPool {
    
    private static HashMap<Integer, CallbackMethod> map = new HashMap<Integer, CallbackMethod>();
    
    private static CallbackPool instance = new CallbackPool();
    private CallbackPool() {        
    }
    public static CallbackPool getInstance() {
        return instance;        
    }
    
    // コールバックメソッド登録
    public void addMethod(Integer cmd, CallbackMethod method) {
        map.put(cmd, method);
    }

   // コールバックメソッド呼び出し    
    public void callMethod(Integer cmd, Integer...params) {
        CallbackMethod method = map.get(cmd);
        if(method.params.length == 0) {
            method.invoke();
        } else {
            method.invoke(params);            
        }            
    }    
}

ざっくりと以上です。

コメント行にも書いてますが厳密にはエラー処理を随所に入れた方がよく、またAndroidのシステムテム的に作法として良いのかどうかも確信は持てません。一つのアイデアとして、DialogFragment表示に画面回転しても落ちない書き方を検討してみた次第です。

DialogFragmentとFragmentのコールバックな関係

DialogFragmentで設定した情報を、呼び出し元のFragmentに反映する方法のお話です。

例えば、あるFragment上にボタンがあるとします。

そのボタンをタップするとダイアログ(DialogFragment)が表示され、そこで何らかの操作ができるとします。

その操作に応じて呼び出し元のFragment上のビューを変更したいとします。

このような動きは、呼び出し元のFragmentとDialogFragmentをコールバックメソッドで連携させることで実現できます。わりと知られている方法かもしれませんが、次回書く予定のエントリーと対比させたいため一応コードを載せてみます。

// 呼び出し元のFragment
public class MyFragment extends Fragment implements OnOkClickListener{

    // ダイアログを表示するボタン
    private Button mDispDialog;
    // ダイアログで選択したものを反映するTextView
    private TextView mSelectedText;
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.my_fragment, container, false);

        mSelectedText = (TextView) v.findViewById(R.id.textSelected);        
        mDispDialog = (Button) v.findViewById(R.id.btnDispDialog);
        mDispDialog.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                FragmentManager manager = getActivity().getSupportFragmentManager();
                MyDialog dialog = MyDialog.newInstance();
                dialog.setTargetFragment(MyFragment.this, 0); // ★★★
                dialog.show(manager, "MyDialog");
            }
        });
        return v;
    }

    // コールバックされるメソッド
    @Override
    public void onOkClicked(Bundle args) {        
      int selectedId = args.getInt("KEY_MYDIALOG");
      String text = "none";
      
      switch (selectedId) {
          case R.id.radioDog:
              text = "いぬ";
              break;
          case R.id.radioMonkey:
              text = "猿";
              break;
          case R.id.radioPheasant:
              text = "キジ";
              break;
          default:
      }
      
      mSelectedText.setText(text.toString());      
    }    
}
// Fragmentから生成されるダイアログ
public class MyDialog extends DialogFragment {

    // 選択されたラジオボタンのID
    int mCheckedId;

    public static MyDialog newInstance() {
        return new MyDialog();
    }

    private OnOkClickListener mListener;
    public interface OnOkClickListener {
        public void onOkClicked(Bundle args);
    }
        
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);        
        mListener = (OnOkClickListener) getTargetFragment();
        if (mListener instanceof OnOkClickListener == false) {
            throw new ClassCastException("実装エラー");
        }        
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        LayoutInflater inflater = getActivity().getLayoutInflater();
        View v = inflater.inflate(R.layout.my_dialog, null, false);
        RadioGroup radioGroup = (RadioGroup) v.findViewById(R.id.radioGroupOptions);

        radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                mCheckedId = checkedId;
            }
        });

        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setTitle("MY DIALOG");
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                // 呼び出し元フラグメントのビューを更新
                Bundle arg = new Bundle();
                arg.putInt("KEY_MYDIALOG", mCheckedId);
                // MyFragmentのonOkClickedをコール
                mListener.onOkClicked(arg);
            }
        });
        builder.setNegativeButton("Cancel", null);
        builder.setView(view);
        return builder.create();
    }
}

今回のサンプルのようなシンプルなアプリであれば上記コードで問題なく動作するのですが、より複雑なレイアウト構造を持ったアプリで同様の実装をしてみると、ダイアログ表示中に画面回転させた時に例外が発生しました。例外の種類及び発生状況は以下のリンク先で述べられているものと似ています。
android - Failure saving state - target not in fragment manager (setTargetFragment) - Stack Overflow

どうやら呼び出し元FragmentのsetTargetFragment(上記コード★★★箇所)で設定したオブジェクトが、画面回転中にロストされたようにも見えます。Androidのシステムではデバイスの向きが変わり画面回転が発生するときは、Activityなどごっそりと再生成されるようで、その過程でsetTargetFragmentで設定したものがロストされたのかもしれません。正確なところは分かりません。

この辺りはつぶさに動作検証したりAndroidのコードを見たりして真の原因を追求すべきものかもしれませんが、実験的に別のロジックでスクリーンショットのものと同じ動作ができるようにしてみました。その内容については次回のエントリーにて書く予定です。

AndroidでFeliCaを読み込むとエラーが起きるとき

今回のエントリーは、AndroidFeliCa内データへのアクセス中にExceptionが出てしまった時の対応についてです。

Android本体にNFCの機能が搭載されている場合、「作法」に従ってコーディングすれば、Suicaなどの交通系FeliCa系カード内の残高や履歴データを参照することが出来ます。

そこで試しに手元にあるPASPY(※)を読み込んでみようと思い、FeliCaリーダーアプリを作っていましたが、データの読み込み中に間欠でExceptionが発生してしまいました。PASPYのみがそのような問題が起きるのか、それともFeliCaカード全般でそうなのかは検証できていませんが、最終的には少なくともPASPYのデータであれば確実に読み込むことが出来ましたので、今回はその問題への対応を書いてみます。

PASPYは広島圏内のバスや路面電車等で使われる交通系ICカードです。

一般に、FeliCaのデータを読み込む処理はAndroidが提供しているNFCタグ周りの実装方法ではなく、FeliCa独自のデータフォーマットを使い独自のアクセスの仕方をする必要があります。詳しくはSONYの「FeliCaカード ユーザーズマニュアル 抜粋版」にその仕様が書かれています。

その仕様によると、FeliCa内の履歴データは16バイトのブロック単位でアクセスする必要があり、アクセスする場合は幾つかのバイトデータでリクエストコマンドを組み立て、それをカードへリクエストします。リクエストコマンドを正しく組み立てることができれば、レスポンスデータが正しく返却されてきますので、それを逐一解析してAndroidのUI要素へバインドすればアプリは出来上がります。

ところが当初、このリクエストコマンドの組み立ての部分が良くなかったようで、時折データアクセス時に例外が発生してしまいました。

//
// エラーが発生する書き方
//
private void readNfc(Intent intent) {
    byte[] cardId = new byte[] { 0 };
    NfcF felica= null;
    Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
    if (tag != null) {
        cardId = tag.getId(); // カードのユニークIDを取得
        felica= NfcF.get(tag);
    } else {
        return;
    }

    // 1リクエストで20件分読み込もうとすると例外が発生
    try {
        // 接続
        felica.connect();

        // リクエストデータの組み立て(20件の履歴データを取得したい)
        byte[] req = buildRequest(cardId, 20);

        // カードにリクエスト送信
        byte[] res = nfc.transceive(req); // ここで例外発生(12件前後までなら読み込める…)

        // クローズ
        nfc.close();

        // 結果の解析
        parse(res);
    } catch (Exception e) {
        Log.e("TEST", e.getMessage(), e);
    }
}

private byte[] buildRequest(byte[] cardId, int blockSize)
        throws IOException {
    ByteArrayOutputStream out= new ByteArrayOutputStream(100);

    out.write(0);
    out.write(0x06); // Read Without Encryptionコマンド(カード内データを読むコマンド)
    out.write(cardId); // カードID
    out.write(1); 
    out.write(0x0f); // 履歴データを取得するための指定
    out.write(0x09); // 履歴データを取得するための指定

    out.write(blockSize); // ※ ここで取得したい全ブロック数を指定しまっていた
    for (int i = 0; i < blockSize; i++) {
        out.write(0x80); // 前述のFelica仕様書に従う
        out.write(i); // ブロック番号
    }

    byte[] byteMsg = out.toByteArray();
    byteMsg [0] = (byte) byteMsg .length;
    return byteMsg ;
}

例えば20件の履歴データを取得するために、1つのRequestで20件全てを取得しようとしたことが良くなかったようです。そこで1つのRequestで1つの履歴データを取得し、それを20回繰り返すという書き方に変えれば、問題なくデータの取得が出来ました。

//
// 正常にデータ取得できた書き方
//
private void readNfc(Intent intent) {
    byte[] cardId = new byte[] { 0 };
    NfcF felica= null;
    Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
    if (tag != null) {
        cardId = tag.getId(); // カードのユニークIDを取得
        felica= NfcF.get(tag);
    } else {
        return;
    }

    // リクエストを1個づつ20回発行
    try {
        for (int i = 0; i < 20; i++) {
            // 接続
            felica.connect();

            // リクエストデータの組み立て
            byte[] req = buildRequest(cardId, i);

            // カードにリクエスト送信
            byte[] res = nfc.transceive(req);

            // クローズ
            nfc.close();

            // 結果の解析
            parse(res);
        }
    } catch (Exception e) {
        Log.e("TEST", e.getMessage(), e);
    }
}

private byte[] buildRequest(byte[] cardId, int addr)
        throws IOException {

    ByteArrayOutputStream out= new ByteArrayOutputStream(100);

    out.write(0);
    out.write(0x06); // Read Without Encryptionコマンド(カード内データを読むコマンド)
    out.write(cardId); // カードID
    out.write(1); 
    out.write(0x0f); // 履歴データを取得するための指定
    out.write(0x09); // 履歴データを取得するための指定

    out.write(1); // ※ 取得したいブロックサイズを1に
    out.write(0x80);
    out.write(addr); // 取得対象のブロック番号

    byte[] byteMsg = out.toByteArray();
    byteMsg [0] = (byte) byteMsg .length;
    return byteMsg ;
}

振り返ってみれば仕様を細部までちゃんと咀嚼していれば良かったのですが、ハマっている最中は色々な事象に囚われて意外と気づきにくいものです。。。

ちなみに、最終的にはこのように出来上がりました。
https://play.google.com/store/apps/details?id=com.kyakujin.android.hmicchecker

PHPのデバッグ時に起動57%で止まってしまう場合の確認ポイント

Eclipse + PHP + Xdebugという、参考書にもよく載っているモジュール構成でPHPのコードをデバッグしようとすると、起動後57%で処理が停止してしまう場面に出くわすことがあります。

ネットで検索すると、この現象にぶつかる方も多いようです。

私もしばらくこの件に煩悶したのですが、解決しました。

結論から言えば、設定ファイルの記述を修正することで解決。

開発環境は、以前のブログに書いたものと同様です。

VMware PlayerのゲストOSからネット接続をするための設定 - KyakujinのWarning Log

私の環境ではXdebugの設定ファイルは下記に格納されており、このファイルを編集しました。

/etc/php.d/xdebug.ini

この設定ファイルの中身を丸々掲載します。

; Enable xdebug extension module
zend_extension=/usr/lib64/php/modules/xdebug.so

; see http://xdebug.org/docs/all_settings

xdebug.remote_enable = 1
xdebug.remote_host = 192.168.11.11
xdebug.remote_handler=dbgp
xdebug.remote_mode=req
xdebug.remote_port=9000

ポイントは次の2点。

  • xdebug.remote_host」の箇所に適切なアドレス(=デバッグ対象のアドレス)を設定すること。
  • xdebug.remote_port」で設定したポート番号がXdebug以外で使われてないか確認すること。

この2点に気をつけて適宜修正し、Apacheを再起動するとphpinfoにxdebugの項目も表示され、PHPデバッグが無事出来るようになりました。

この話題は様々な方が解決法を書いていらっしゃるのですが、環境が微妙に違うためか記載通りに設定してもうまくいかないことが多かったので、自分の環境を再インストールして同じ問題にぶつかった時のために、今のうちに記録しておこうと思ったのです。

VMware Playerがエラーで起動できない時の対応

VMware Playerが起動できない時があります。

私の場合はWindows7VMware Playerをインストールしているのですが、いつものようにVMware Playerのアイコンをクリックすると、エラーダイアログが出て起動できない時があります。初めて見たときは焦ります。

このエラーが出る原因は、VMware Playerに必要なサービスが起動していないから。そのサービスはエラーメッセージにも出ているように「VMware Authorization Service」というもので、通常は自動で起動されるものですが、何らかのタイミングで起動できないことがあるようです。もしくは何らかのソフトウェア的割り込みが原因かもしれません。

ともかく、そのサービスを手動で起動した後、VMware Playerのアイコンをクリックすれば無事に仮想マシンを使用することが出来ます。「管理ツール」-「コンピューターの管理」-「サービス」へと辿り、「VMware Authorization Service」を選択して右クリックから手動でサービスが起動出来ます。

簡単なようですが、初めてこのエラーを見たときは「仮想マシン壊れたか…」とプチ絶望に陥りますので、忘れた時のために今のうちに書き留めておきます。

Androidアプリのソースコード公開

先月このブログでもお知らせしましたが、節電系ユーティリティアプリ「オート節電」をGoogle Playに公開しました。

https://play.google.com/store/apps/details?id=com.kyakujin.android.autoeco&hl=ja

Androidアプリの入門書を読み終え、次により実践的なアプリを作成してみたいと思われる方もいると思いましたので、何らかの参考になるかと思いソースコードを公開することにしました。

ソースは以下に公開しております。

GitHub-AutoEco

Androidアプリを作成するにあたってよく使われる基本的な実装技術は、おおよそ盛り込んでいます。

具体的に幾つか挙げると、

・リストビュー
・フラグメント
・データの永続化(SQLiteやPreference)
・カーソルローダー
・サービスの起動
・ブート時の処理
・アラーム管理
インテントによるデータ受け渡し

といった所です。

他にも、今までブログで公開してきた実装例(時間設定ダイアログ等)や、AdMobによる広告配信の実装例についても、このアプリに組み込んでいます。(実際に動かすには、AdMobのライブラリが別途必要になります。)

ただし、当初はソースコードを公開する予定はありませんでしたので、所々勢いで書き連ねた部分もあり、またコメント行も大雑把な所も多々あって読みづらい箇所があるかもしれませんが、そのあたりはご了承ください。

分かりにくいところは、おいおい修正するかもしれません。

なお、いわゆるオブジェクト指向デザインパターンを幾つか適用していますので、分かりにくいところはデザインパターンの入門書を参照して頂ければ、ソース解読しやすくなるかと思います。以下の書籍が分かりやすくオススメです。

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

節電系ユーティリティアプリをリリースしました

本日、Androidアプリを一つ、Google Playに公開しました。

端末本体の節電系ユーティリティアプリです。

https://play.google.com/store/apps/details?id=com.kyakujin.android.autoeco&hl=ja

スクリーンショットの一部です。


よく知られているように、スマートフォン端末本体のバッテリー消費量は、ディスプレイ輝度設定やWi-Fiなどの通信処理によって大きく影響されます。そのため、バッテリー消費量を抑えたい場合は、端末の設定画面からそれらのバッテリー消費が多そうな項目を一つ一つ設定していくわけですが、これが非常に面倒。

そこで、時間指定やバッテリーしきい値を指定することで、事前に設定しておいたそれら設定内容を、自動的に実行したいという自分の要望から作成してみました。

よくある節電系アプリですが、スケジューラ+バッテリー残量連動で起動するシンプルな操作性のアプリはあまり見かけなかったので作成した次第です。

ちなみに、今までこのブログで書いてきた実装方法のいくつかを実際にこのアプリで適用しています。