初めてのAndroidアプリで躓いたところ【1:アプリ編】（プログラミング初学者）

## アプリの紹介・経緯とか
Androidアプリの初めての成果物として、テキストを読み上げるアラームを作りました。
既存のテキスト読み上げアラームは「アラームが起動中」に読み上げるので、
全文を聴き終える前に停止するのを防ぐ意味で「アラームを停止後」に読み上げるように作りました。
このアラームを作る過程でつまづいた、分かり辛かったことについて書きます。
[作成したアプリはこちら](https://play.google.com/store/apps/details?id=com.hino.speakalarm)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330156/ff984a90-990e-2015-0799-d1ff2602482b.png" width=30%>

## 音声編
### TextToSpeecnの初期化・リソースの解放
onCreate中で`TextToSpeech(this, this)`で初期化する必要があります。
また、onDestroyでリソースを解放しないとエラーが出ます。(ServiceConnectionLeaked)

```AlarmBootActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
        ....
        // TextToSpeechの初期化、初期設定
        tts = TextToSpeech(this, this)
        ....
}

override fun onDestroy() {
    super.onDestroy()
    tts.shutdown() // ttsのリソースを解放
    ....
}
```

### Androidの音量設定と分ける
Androidの端末の音量設定を操作するので、
`onCreate`・`onResume`で端末の音量を変数に保持します。
`onStop`・`onPause`・`onDestroy`で元の音量に戻し、`onResume`でアプリの設定値に戻します。
しかし、**onCreateから起動した場合は、onRsumeでは端末の音量を取得しないようにします。**
（アプリで設定した音量を取得してしまうので）

```AlarmBootActivity.kt
private var preMusicVol: Int? = null // 端末の元の音量設定(アラーム音: メディアの音量)
private var preVoiceVol: Int? = null // 端末の元の音量設定(テキスト読み上げ: 着信音の音量)
private var musicVol: Int? = null // アラームの音量
private var voiceVol: Int? = null // 声の音量
private var onCreateMark: Boolean? = null // onCreateからの起動かを判定
override fun onCreate(savedInstanceState: Bundle?) {
        ....
        // 現在の端末の音量設定を格納(onDestroy()・onPause()・onStop()で元に戻す)
        getPreVolumeConfig()
        // onCreateから起動したことを確認する
        onCreateMark = true
        ....
}

override fun onPause() {
    super.onPause()
    when (onCreateMark) {
        true -> onCreateMark = false
        false -> {
            // 現在の端末の音量設定を取得
            getPreVolumeConfig()
            // シークバーの音量設定に戻す（アラーム音）
            musicVol = musicVolSeekbar.progress
            // シークバーの音量設定に戻す（テキスト読み上げ音）
            voiceVol = voiceVolSeekbar.progress
        }
    }
}

override fun onStop() {
    super.onStop()
    preVolumeSet() // 元の音量設定に戻す
}

override fun onDestroy() {
    super.onDestroy()
    preVolumeSet() // 元の音量設定に戻す
}

// 現在の端末の音量設定を格納(onDestroy()・onPause()・onStop()で元に戻す)
private fun getPreVolumeConfig() {
    preMusicVol = am.getStreamVolume(STREAM_NOTIFICATION) // アラーム音
    preVoiceVol = am.getStreamVolume(STREAM_MUSIC) // テキスト読み上げ音
}

// アラーム音・テキスト読み上げ音の音量設定を戻す
private fun preVolumeSet() {
    am.setStreamVolume(STREAM_NOTIFICATION, preMusicVol!!, 0)
    am.setStreamVolume(STREAM_MUSIC, preVoiceVol!!, 0)
}
```


## カレンダー編
現在時刻を取得する場合、
毎回`Calendar.getInstance()`でカレンダーを取得する必要があります。
`timeInMillies()`はカレンダーを取得した時点での現在時刻を取得するので、
数秒前に`Calendar.getInstance()`で取得した場合、数秒前が現在時刻となるので注意。


## アラーム編
### 再起動時にアラームを再設定する（ダイレクトブート）
Android起動時のダイレクトブートモードで、アラームを再設定するReceiverを呼び出します。
呼び出すReceiverにはAndroidManifestで以下のように記述します。

```AndroidManifest.xml
<receiver
        android:name=".DirectBootReceiver"
        android:directBootAware="true"
        android:enabled="true">
    <intent-filter>
        <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>
```

指定したReceiverでDBから「アラーム」がONになっているデータだけ取得してアラームを再設定します。

※ダイレクトブートで呼び出したReceiverからアクティビティを呼び出すことはできますが、**Serviceを呼び出すことはできません。**
（「サービスの開始を許可しないIntentです」と怒られます。）

### 予約した自動スヌーズの遅延処理をキャンセルする
サービスを破棄するときに自動でスヌーズを実行する遅延処理をキャンセルします。
これを忘れるとサービスを破棄された後でも遅延処理は実行されてしまいます。

```ForegroundService.kt
private var runSnooze: Runnable? = null // 遅延処理でアラームを自動スヌーズ

val snoozeHandler = Handler()
runSnooze = Runnable {
    // スヌーズ実行
    val snoozeIntent = Intent(this, AlarmSnoozeBroadcastReceiver::class.java)
    sendBroadcast(snoozeIntent)
}
snoozeHandler.postDelayed(runSnooze!!, 60000)

override fun onDestroy() {
    super.onDestroy()
    // タップしてアラームを停止させた場合、1分後のスヌーズ処理をキャンセル
    snoozeHandler.removeCallbacks(runSnooze!!)
}
```

### setRepeatが使えない。
`setRepeat`はAPI19以降から不正確なので推奨されていません。
なのでアラームを止めた後に、
同じrequestCodeを使って`set()`・`setExact()`・`setAlarmClock()`でアラームを再設定しました。

### Android10ではActivityをバックグランドから呼べない
Android9.0以下は、アラーム起動時にActivityを呼ぶようにしていますが、
Android10はバックグランドからActivityを呼べないのでServiceを呼ぶようにしています。
サービスも`startForegroundService()`で呼ぶ必要があります。
ForegroundServiceについてはこちらを参考にしました。
[Foreground Serviceの基本]
(https://qiita.com/naoi/items/03e76d10948fe0d45597)

### 通知の削除
`setAutoCancel(true)`や`Notification.FLAG_AUTO_CANCEL`では通知をタップしても何故か消えなかったので、チャンネルIDを指定して通知を削除するようにしました。

```ForegroundService.kt
override fun onDestroy() {
    super.onDestroy()
    // アラームを停止させたら、サービス終了時に通知を消す。
    val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    manager.deleteNotificationChannel("alarm_notification") // アラームの通知を削除
}
```

## Realm・RecyclerView編
### requestCodeをどうやって分けるか
それぞれ月〜日曜日の`requestCode`を保存するカラムを用意しました。
配列で保存しようとも考えましたが、
RDBは基本的に配列を記録させられないので単一のデータごとに保存しました。
※1桁の数字だけを配列にするなら、文字列にして保存した後に`contains('2')`で検索すれば取得できなくもありません。
![Screenshot 2019-10-10_21-17-50-502.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330156/bdcf8801-3c8e-f0bb-eea0-2fdb2f6c6787.png)

### 削除の演出
RecyclerViewに表示されている登録したアラームを削除するときに、
`notifyItemRemoved()`と`notifyDataSetChanged()`で削除＆更新ができます。
（最初なぜか上手く実行できませんでした）

```CustomRecyclerViewAdapter.kt
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    ....
    notifyItemRemoved(position) // アラームの位置を取得して消す
    notifyDataSetChanged() // Realmのデータが変更されたら自動的に更新する
    ....
}
```

### RealmのDB内容の確認
Realm Studio(Mac)での確認方法はいくつか解説しているサイトがありましたが、
自分はあまりうまくいきませんでした。
最終的にこれで確認できました。↓
[Realm StudioでDBの内容を確認する方法【Android・Mac】]
(https://qiita.com/tanegashimav48273/items/9f6e27e4c20126f94e75)

## まとめ
初めてのAndroidアプリ開発ということで、
Androidの特有の機能やバージョン間の違いを理解するのに苦労しました。

次はアプリのリリース編を書きます。
[初めてのAndroidアプリで躓いたところ【2:リリース編】（プログラミング初学者）](https://qiita.com/tanegashimav48273/items/9ec2eb233846690ba93d)