Receiver not registered

拙作のカレンダビュー(CalendarView)では、ViewFlipperにより月の前後の変化をアフォードするアニメーションを実装しているが、レイアウトを縦|横と変えていると以下の例外が発生することに気がついた。(Android 2.1)

: Uncaught handler: thread main exiting due to uncaught exception
: java.lang.IllegalArgumentException: Receiver not registered: android.widget.ViewFlipper$1@43c1cbd0
:        at android.app.ActivityThread$PackageInfo.forgetReceiverDispatcher(ActivityThread.java:667)
:        at android.app.ApplicationContext.unregisterReceiver(ApplicationContext.java:747)
:        at android.content.ContextWrapper.unregisterReceiver(ContextWrapper.java:321)
:        at android.widget.ViewFlipper.onDetachedFromWindow(ViewFlipper.java:104)
:        at android.view.View.dispatchDetachedFromWindow(View.java:5835)
:        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:1076)
:        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:1074)
:        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:1074)
:        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:1074)
:        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:1074)
:        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:1074)
:        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:1074)
:        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:1074)
:        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:1074)
:        at android.view.ViewRoot.dispatchDetachedFromWindow(ViewRoot.java:1570)
:        at android.view.ViewRoot.doDie(ViewRoot.java:2556)
:        at android.view.ViewRoot.die(ViewRoot.java:2526)
:        at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:218)
:        at android.view.Window$LocalWindowManager.removeViewImmediate(Window.java:436)
:        at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:3498)
:        at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3599)
:        at android.app.ActivityThread.access$2300(ActivityThread.java:119)
:        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1867)
:        at android.os.Handler.dispatchMessage(Handler.java:99)
:        at android.os.Looper.loop(Looper.java:123)
:        at android.app.ActivityThread.main(ActivityThread.java:4363)

画面のレイアウトのLandscape|Portraitを変えることでActivityは再生成されるのは解っているのだが、"Receiver not registered"はそれともあまり関係が無さそうにも見えるが...

どうやら例外の根本原因はこの辺にありそう。

:        at android.widget.ViewFlipper.onDetachedFromWindow(ViewFlipper.java:104)

こういう時はオープンソースの最大の利点「困った時はソースコードを見ろ」に従うに限る。
(手元のソースが古いようなので改めてgit cloneし直した)

    • ViewFlipper#onDetachedFromWindow
@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    mVisible = false;

    getContext().unregisterReceiver(mReceiver);
    updateRunning();
}

対になるイベントonAttachedToWindowメソッドを見ると解るが、ViewFlipperは「機器の画面Off」と「ユーザによる機器アクティブ(ロック解除)」アクションに呼応するようにインテントフィルタが動的に登録/解除されているようだ。

    • ViewFlipper#onAttachedToWindow
@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();

    // Listen for broadcasts related to user-presence
    final IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_SCREEN_OFF);
    filter.addAction(Intent.ACTION_USER_PRESENT);
    getContext().registerReceiver(mReceiver, filter);

    if (mAutoStart) {
        // Automatically start when requested
        startFlipping();
    }
}

実際に画面の縦|横を切り替えた際にどのように呼ばれているか(呼ばれていないか)を確認してみる必要があるが、両メソッドともイベントリスナをアタッチすることができない(そもそも実装されてない)ため、拡張したクラスを書くしか手段が無い(この辺酷い設計だと思うんで、Floyoで直っているといいな)

実際にViewFlipperを拡張してテストしてみた。実際に動かしてみると

    • 初回起動時

  -> onAttachedToWindow

    • レイアウトをLandscapeに変更(左CTRL + F11)

  -> onDetachedFromWindow -> onAttachedToWindow

これは問題が無いが、ここから

    • レイアウトをPortraitに変更(左CTRL + F11)

  -> onDetachedFromWindow -> onDetachedFromWindow ×

と見事に onAttachedToWindowとonDetachedFromWindowの対応が崩れていることが解る。

通常、このようにライフサイクルにおいて対で使わなければならないようなメソッドはきっちりテストをした上で、どちらかしか呼ばれなかった場合と、連続で呼ばれた場合にエラーが出ないように書いておくべきであり、これは恐らくはバグだろう。

以下のように、取り敢えず発生した例外を無視することで異常終了を回避することはできるが、

@Override
protected void onDetachedFromWindow() {
    try {
        super.onDetachedFromWindow();
    } catch (IllegalArgumentException e) {
        //ignore
    }
}

あまりといえばあまりだよなぁ。

#海外では既にFAQのようだな。