ForegroundDispatchとonPauseに注意

Android 2.3.3(4)、NFCのForegroundDispatchを制御するにはActivityのonResume/onPauseを基準にする必要があるということで、以下のようなスニペットがよく紹介されている。

    private NfcAdapter mAdapter;
    private PendingIntent mPendingIntent;
    private IntentFilter mFilters;
    private String mTechLists;
    private TextView mText;
    private int mCount = 0;

    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        mAdapter = NfcAdapter.getDefaultAdapter(this);
        mPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
        IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
        try {
            ndef.addDataType("*/*");
        } catch (MalformedMimeTypeException e) {
            throw new RuntimeException("fail", e);
        }
        mFilters = new IntentFilter {
                ndef,
        };
        mTechLists = new String { new String[] { NfcF.class.getName() } };
    }
    @Override
    public void onResume() {
        mAdapter.enableForegroundDispatch(this, mPendingIntent, mFilters, mTechLists);
        super.onResume();
    }
    @Override
    public void onPause() {
        mAdapter.disableForegroundDispatch(this);
        super.onPause();
    }

私も実際にこの通りコーディングしたのだが、この方法はケースによっては上手くいかない。例えば次々と連続して複数のNFCタグを読み込むような処理の場合、有効にしたはずのForegroundDispatchが無効になり、Androidにプリインストールされている汎用のNFCTaggerがタグを横取りしてしまうだろう。

ここで注意しなくてはならないのはonPauseメソッドである。onPauseは通常システムが現在前面にいる以外のActivityを再開する場合に呼ばれるのだが、それだけではなく、現在のActivityが新たなインテントを処理する際にも呼ばれる場合があるため、NFCのForegroundDispatchのようにPendingIntentを使ってタグの検出を処理する場合、インテントをハンドルした時点でonPuaseが呼ばれて、結果としてForegroundDispatchが無効になってしまうのだ。

意図せずに呼ばれているonPauseメソッドのスタックトレース (NfcAdapter.ACTION_TECH_DISCOVEREDアクションのインテント受信時)

NfcFeliCaTagFragment.onPause() line: 139	
FragmentManagerImpl.moveToState(Fragment, int, int, int) line: 853	
FragmentManagerImpl.moveToState(int, int, int, boolean) line: 991	
FragmentManagerImpl.moveToState(int, boolean) line: 974	
FragmentManagerImpl.dispatchPause() line: 1654	
NFCFeliCaReader(FragmentActivity).onPause() line: 375	
NFCFeliCaReader(Activity).performPause() line: 3851	
Instrumentation.callActivityOnPause(Activity) line: 1191	
ActivityThread.performNewIntents(IBinder, List) line: 1732	
ActivityThread.handleNewIntent(ActivityThread$NewIntentData) line: 1742	
ActivityThread.access$2300(ActivityThread, ActivityThread$NewIntentData) line: 117	
ActivityThread$NewIntentData(ActivityThread$H).handleMessage(Message) line: 978	
ActivityThread$H(Handler).dispatchMessage(Message) line: 99	
Looper.loop() line: 130	

これでは一度だけはNFCタグを読み込めるものの、2回目以降はForegroundDispatchが無効になってしまう。
これを回避するには、ForegroundDispatchを無効にするのはを明確にすることだ。例えばActivityが完全に終了したら無効にするのであれば、onResumeメソッドはそのままでonPauseメソッドを以下のように書き換える。

    @Override
    public void onPause() {
        if ( this.isFinishing() ) {
            mAdapter.disableForegroundDispatch(this);
        }
        super.onPause();
    }

これで対象のActivityが終了するまでForegroundDispatchを有効にしたままにできる。