アプリの紹介・経緯とか
Androidアプリの初めての成果物として、テキストを読み上げるアラームを作りました。
既存のテキスト読み上げアラームは「アラームが起動中」に読み上げるので、
全文を聴き終える前に停止するのを防ぐ意味で「アラームを停止後」に読み上げるように作りました。
このアラームを作る過程でつまづいた、分かり辛かったことについて書きます。
作成したアプリはこちら
音声編
TextToSpeecnの初期化・リソースの解放
onCreate中でTextToSpeech(this, this)
で初期化する必要があります。
また、onDestroyでリソースを解放しないとエラーが出ます。(ServiceConnectionLeaked)
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では端末の音量を取得しないようにします。
(アプリで設定した音量を取得してしまうので)
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で以下のように記述します。
<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です」と怒られます。)
予約した自動スヌーズの遅延処理をキャンセルする
サービスを破棄するときに自動でスヌーズを実行する遅延処理をキャンセルします。
これを忘れるとサービスを破棄された後でも遅延処理は実行されてしまいます。
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を指定して通知を削除するようにしました。
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')
で検索すれば取得できなくもありません。
削除の演出
RecyclerViewに表示されている登録したアラームを削除するときに、
notifyItemRemoved()
とnotifyDataSetChanged()
で削除&更新ができます。
(最初なぜか上手く実行できませんでした)
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:リリース編】(プログラミング初学者)