2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-10-11

アプリの紹介・経緯とか

Androidアプリの初めての成果物として、テキストを読み上げるアラームを作りました。
既存のテキスト読み上げアラームは「アラームが起動中」に読み上げるので、
全文を聴き終える前に停止するのを防ぐ意味で「アラームを停止後」に読み上げるように作りました。
このアラームを作る過程でつまづいた、分かり辛かったことについて書きます。
作成したアプリはこちら

音声編

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の端末の音量設定を操作するので、
onCreateonResumeで端末の音量を変数に保持します。
onStoponPauseonDestroyで元の音量に戻し、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

削除の演出

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:リリース編】(プログラミング初学者)

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?