Androidで毎秒カウントダウンをするappWidgetを作ってハマりまくった話

この記事は[フラー Advent Calendar 2020](https://adventar.org/calendars/5034)の 10 日目の記事です。

##この記事で何が言いたいか
最終的な着地点は「ちゃんと技術的な見積もりをしてから開発のGOを出さないと痛い目を見るよ」という話です。
そんな常識的なことはわかってるから、技術的にどうやって作ったんやって方は[後半パート](https://qiita.com/seto_inugami/items/9762f4a66eddfaacb19a#%E6%9C%80%E5%BE%8C%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AD%E3%83%BC%E3%83%81-remoteviewsservice "後半パート")
まで飛んでください。

##技術見積もり
今回の開発はタイトルにある通り「毎秒カウントダウンするappWidget」を開発して欲しいという案件でした。
開発が規模的にそんなに大きくない＆開発に時間がかけられないということもあり、今回は「すでに同等の機能がついているアプリがストアに存在するか」「公式ドキュメントをみていけそうか」という二つの観点でざっくり「いける」「いけない」を見積もることにしました。

####すでに同等の機能がついているアプリがストアに存在するか
こちらに関してはいくつかあったのですが、ざっと調べたところ不安定なものが多い印象でした。
アプリをバックグラウンドでキルすると止まっているアプリ、Widgetを表示してしばらく置いておくと勝手にカウントダウンが止まっているアプリなどが散見されました。
ただ、その中でもちゃんと動いているアプリがいくつかあったので、技術的には可能で難易度もそこそこかなと判断しました。

####公式ドキュメントをみていけそうか
こちらもいけそうだなと判断しました。
```updatePeriodMillis```は最速でも30分間隔でしか刻めないことは理解していて、その上で以下の公式ドキュメントを読んで「更新頻度を高くする場合はAlarmManagerを使えば何とかなりそう」という結論に至りました。（AlarmManagerなところが肝要）
https://developer.android.com/guide/topics/appwidgets?hl=ja#MetaData

##最初のアプローチ (```Observer.interval```)
問題なくいけそうだという判断を自分の中で下したので、ディレクターの方には色良い返答をして実装開始です。
```AlarmManager```を使えと書いてありはしましたが、技術が課題に対してちょっと大きいなと思ったので最初は```countDownTimer```や```Observer```を使ったループで毎秒カウントを実現し、それを```AppWidgetProvider```内で動かすといった形の実装を取ってみました。

こんなやつを```AppWidgetProvider```のonUpdateから呼ぶ感じです。

```kotlin
private fun setCountDownTimer(context: Context, appWidgetId: IntArray) {
        Observable.interval(TIMER_COUNT_DOWN_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
            .timeInterval()
            .observeOn(AndroidSchedulers.from(Looper.myLooper(), true))
            .subscribe {
                val intent = Intent(context, ClockWidget::class.java)
                intent.action = ACTION_UPDATE
                val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0)
                pendingIntent.send()
            }.addTo(disposable)
    }
```

しかしこの実装だと、あくまでカウントダウンは```AppWidgetProvider```上で動いているので、アプリがKillされるとWidgetのカウントダウンも止まってしまうことがわかったので、無事不採用になりました。

##次のアプローチ (```Chronometer```)
では発想を変えて、UI側から攻めれないかと思って```Chronometer```の利用を考えました。
しかし、「カウントダウンする値が１年刻みとかなり大きい点」や「カウントダウン終了後、数時間して別の処理を行う必要がある点」などの要件があり、それを考慮したときに不可能だという判断に至りました。

##さらに次のアプローチ (```AlarmManager```)
ならしょうがないと、公式ドキュメントのいう通り、```AlarmManager```を使って実装してみました。
```AlarmManager```はアプリの実行期間外に時間ベースの処理を行うことができるので、最初のアプローチの問題は解決できるはずです。

こんなやつ

```kotlin
val alarmManager: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val intent = Intent(context, WidgetService::class.java)
        pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)
        val time: Calendar = Calendar.getInstance()
        time.set(Calendar.MINUTE, 0)
        time.set(Calendar.SECOND, 0)
        time.set(Calendar.MILLISECOND, 0)
        alarmManager.setRepeating(AlarmManager.RTC, time.time.time, 60 * 1000, pendingIntent)
```

えぇ、勘の良い方はおわかりだと思います。
```AlarmManager```はそもそも正確性を求めて使うものではなく、呼び出し方にもよりますが**最速でも5秒間隔でしか呼び出すことができません。**
また、端末の状態（Dozeモードなど）の影響も受けやすく、端末を放置すればするほど挙動は不安定になっていったためこちらも不採用。

##さらに次の次のアプローチ (```ForegroundService```)
実装の時間が取れないけど、思ったより難易度が高いんじゃないかとこの辺から焦り始めます。
納期も迫っていたため、妥協案として通知領域に常に「カウントダウン中です」と言った表示をしつつ、Serviceを使ってWidgetでカウントダウンをするという方向をディレクターと探り始めたのがこの時期でした。

結局```ForegroundService```使う許可は降り、実装的にも問題ないところまでできたのですが、その後に解決策を思いついたのでお蔵入りになりました。

##最後のアプローチ (```RemoteViewsService```)
結論として```RemoteViewsService```を使って解決することができました。
```RemoteViewsService```はもともとAppWidgetに切り替え可能なリストを提供するためのServiceで```RemoteViewsFactory```とセットで使います。
そして```RemoteViewsService```の実装はServiceをExtendしており、尚且つ通知領域に通知を出し続けなくて良い形になっているのです。
なので、```RemoteViewsService```上で```Observer```でカウントを行い、毎回```notifyAppWidgetViewDataChanged```を発行して```RemoteViewsFactory```からRemoteViewを返してもらうという形の実装を行い、ギリギリ実装を終えることができました。

実際の実装（一部抜粋）

```kotlin
class WidgetRemoteViewsService : RemoteViewsService() {
    private val disposable = CompositeDisposable()

    override fun onCreate() {
        super.onCreate()
        val appWidgetManager = AppWidgetManager.getInstance(this)
        setCountDownTimer(appWidgetManager)
    }

    override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
        return WidgetRemoteViewsFactory(this)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return START_REDELIVER_INTENT
    }

    override fun onDestroy() {
        disposable.dispose()
        super.onDestroy()
    }

    private fun setCountDownTimer(
        appWidgetManager: AppWidgetManager
    ) {
        Observable.interval(TIMER_COUNT_DOWN_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
            .timeInterval()
            .observeOn(AndroidSchedulers.from(Looper.myLooper(), true))
            .subscribe {
                // Loop中にWidgetを追加されたときに追加されたWidgetのカウントダウンも正常に動かすためにこのタイミングでappWidgetIdsを読み込む
                val appWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(this, ClockWidget::class.java))
                appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.adapterCountDownView)
            }.addTo(disposable)
    }
```
```kotlin
class WidgetRemoteViewsFactory internal constructor(private var context: Context) : RemoteViewsFactory {
    
    private fun createWidgetRemoteViews(): RemoteViews {
        val remoteViews = RemoteViews(context.packageName, R.layout.widget_clock)
        val timeZone = ZoneId.of("Asia/Tokyo")
        val countDownMillis = xxCountDownTimer.getCountTime(ZonedDateTime.now(timeZone))
        val duration = Duration.ofSeconds(countDownMillis)
        remoteViews.setTextViewText(R.id.countDownTextView, DurationStringConverter().convertDurationString(duration))
        return remoteViews
    }
}
```

##終わってから考えたこと
まぁ冒頭でも言いましたが、「ちゃんと技術的な見積もりをしてから開発のGOを出さないと痛い目を見るよ」という一言に尽きるなと思いました。
通常のアプリ内での実装であれば、まだ結構先駆者もいてなんだかんだ何とかなったりするイメージはありますが、Widgetに関してはあまり技術文献もネットに落ちておらず、Githubのソースコードを結構深くまで漁ったりとかもしました。
その技術が新しいものか、ネットに文献が落ちているかどうかも見積もり要素としていれたほうがいいなと改めて思い知らされました。

私の経験が、誰かのために役に立ったら良いなとおもいここに書き連ねておきます。