Android 4系のDatePickerDialog, TimePickerDialogに存在するバグ

# はじめに
私が最近遭遇したDatePickerDialog, TimePickerDialogに存在するバグについて書きます。
この記事では以下の2つの問題に触れます。

- Android 4.1〜4.4のDatePickerDialog, TimePickerDialogにはバグがある
- Android 4系の一部端末にはTimePickerDialogにキャンセルボタンがあるが、onCancelが呼ばれない

# Android 4.1〜4.4のDatePickerDialog, TimePickerDialogにはバグがある
## バグの概要
DialogのdoneをタップするとcallbackメソッドのonDateSet, onTimeSetが2回呼ばれます。
（期待する動作は1回呼ばれること）

バックボタンやDialog外をタップするなど、dismissの時は1回呼ばれます。
（期待する動作は呼ばれないこと）

https://code.google.com/p/android/issues/detail?id=34833
にissueが立てられていて、Android 4.1〜4.4に存在するバグなこと、5で直ったことなどが書かれています。

## どう回避するか
私は下記の ```View.isShown()``` を使う方法がシンプルだったためこれを採用しました。
ただし後述する別の問題があるため、Android 4.1〜4.4に限定するのがよいです。
http://stackoverflow.com/a/26034036

こんな感じのコードです。

```java
@Override
public void onTimeSet(TimePicker dialog, int hourOfDay, int minute) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
        Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT &&
        !dialog.isShown()) {
        // workaround
        return;
    }

    // callbackでやりたい処理
}
```

ここで気をつけないといけないのが

> but this logic not working in samsung 4.0.4 version. view.isShown() always returns false. – Shanki Bansal Jan 27 '15 at 5:59

というコメントです…。
```View.isShown()```が常にfalseを返す端末があるようで、それを除外しないといけません。
上記だとSamsungの4.0.4と書かれていますが、Samsungの4.0.3の端末でも同様に ```view.isShown()``` が常にfalseを返すらしき事象に遭遇しました。
（お客様からのお問い合わせに書かれていた症状からの推測です）
手元にあった非Samsungの4.0.3な端末では再現しなかったので、Samsungの4.0.xな端末が怪しそうです。

そのためAndroid 4.1〜4.4に限定しています。
元々の不具合も4.1〜4.4で発生しているものなので、workaroundはなるべく対象を絞って副作用の可能性を下げるのがよいですね。

（私ははじめこの上記コメントを見逃していて、OSのバージョンを絞らずリリースをしていました＞＜）

# Android 4系の一部端末にはTimePickerDialogにキャンセルボタンがあるが、onCancelが呼ばれない
## バグの概要
Android 4系の一部端末で、TimePickerDialogのボタンが普通はPOSITIVEボタンの「設定」しかないものの、一部端末は「キャンセル」というボタンも表示される。
しかしこの「キャンセル」ボタンをタップしてもonCancelが呼ばれない…というバグです。

```java
TimePickerDialog tpd = new TimePickerDialog();
tpd.setOnCancelListener(getOnCancelListener()); //ここでsetOnCancelListenerしていてもonCancelが呼ばれない

private TimePickerDialog.OnCancelListener getOnCancelListener({
    return new TimePickerDialog.OnCancelListener() {
        @Override
        public void onCancel(DialogInterface dialog) {
            cancel();
        }
    };
}

private void cancel() {
    //キャンセルボタンがタップされた時にしたい処理
}
```

AndroidのBug Reportにはそれらしきissueが見つからないこと、問題のある実機ではキャンセルボタンが表示されているのに、同じOSバージョンのシミュレータを起動するとキャンセルボタンが無いことから、メーカーカスタマイズによるバグなんじゃないかなあと思っています。

## どう回避するか
下記のように明示的にキャンセルボタンを追加してやることで解決できました。

```java
TimePickerDialog tpd = new TimePickerDialog();
tpd.setOnCancelListener(getOnCancelListener()); //ここでsetOnCancelListenerしていてもonCancelが呼ばれない
tpd.setButton(DialogInterface.BUTTON_NEGATIVE, "キャンセル", getWorkaroundCancelListener());

private TimePickerDialog.OnCancelListener getOnCancelListener({
    return new TimePickerDialog.OnCancelListener() {
        @Override
        public void onCancel(DialogInterface dialog) {
            cancel();
        }
    };
}

private DialogInterface.OnClickListener getWorkaroundCancelListener() {
    return new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            cancel();
        }
    };
}

private void cancel() {
    //キャンセルボタンがタップされた時にしたい処理
}
```