【Androidアプリ開発】サービスと通知

2023年2月7日

前回は、「リストビューのカスタマイズ」を行いました。

【Androidアプリ開発】リストビューのカスタマイズ
【Androidアプリ開発】リストビューのカスタマイズ
前回はDataPickerDialogとTimePickerDialogをやりました。 今回は、リストビューを…
http://nomux2.net/post-2331/

今回はバッググラウンドで動作するサービス、それと通知をやりたいと思います。

通知は別にサービスから呼ばなければいけないということはありませんが、通知を使用するタイミングはだいたいサービスからが多いと思いますのでついでに説明しようと思います。

サービス

サービスを使用するためのマニフェストファイルです。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.todomanager">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <service
            android:name=".TodoAlarmService"
            android:enabled="true"
            android:exported="false"></service>

        <activity android:name=".TodoCategoryManager" />
        <activity android:name=".TodoDetailActivity" />
        <activity android:name=".TodoUpdateActivity" />
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

サービスを継承したクラスです。

package com.example.todomanager

import android.app.*
import android.content.Context
import android.content.Intent
import android.os.IBinder
import androidx.core.app.NotificationCompat

class TodoAlarmService : Service() {

    val INTERVAL_PERIOD = 5000
    val NOTIFICATION_ID = "todo_alarm_service_notification_channel"

    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }

    override fun onCreate() {
        super.onCreate()

        //通知チャンネルのID文字列を用意
        val id = NOTIFICATION_ID
        //通知チャンネル名
        val name = "test_notification"
        //通知チャンネルの重要度を標準に設定
        val importance = NotificationManager.IMPORTANCE_DEFAULT
        //通知チャンネルを生成
        val channel = NotificationChannel(id, name, importance)
        //NotificationManagerオブジェクトを取得
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        //通知チャンネルを設定
        manager.createNotificationChannel(channel)
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {

        execAlarm()

        scheduleNextTime(intent)

        return Service.START_STICKY
        //return super.onStartCommand(intent, flags, startId)
    }

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

    fun execAlarm() {

        //Notificationを作成するBuilderクラスの生成
        val builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_ID)
        //通知エリアに表示されるアイコンを設定
        builder.setSmallIcon(android.R.drawable.ic_dialog_info)
        //通知ドロワーでの表示タイトルを設定
        builder.setContentTitle("TodoManager")
        //通知ドロワーでの表示メッセージを設定
        builder.setContentText("タイマーです。")
        //起動先Activityクラスを指定したIntentオブジェクトを生成
        val intent = Intent(applicationContext, MainActivity::class.java)
        //起動先Activityに引き継ぎデータを格納
        intent.putExtra("fromNotification", true)
        //PendingIntentオブジェクトを取得
        val stopServiceIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)
        //PendingIntentオブジェクトをビルダーに設定
        builder.setContentIntent(stopServiceIntent)
        //タップされた通知メッセージを自動的に消去するように設定
        builder.setAutoCancel(true)
        //BuilderからNotificationオブジェクトを生成
        val notification = builder.build()
        //NotificationManagerオブジェクトを取得
        val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        //通知
        manager.notify(0, notification)
    }

    fun scheduleNextTime(intent :Intent) {
        //現在のシステム時間を取得
        val now = System.currentTimeMillis()
        //アラーム用のPendingIntentを取得
        val alarmIntent = PendingIntent.getService(this, 0, intent, 0)
        //AlarmManagerを取得
        val am = this.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        //次回サービス起動を予約
        am.set(AlarmManager.RTC, now + INTERVAL_PERIOD, alarmIntent)

    }
}

サービスの起動部分です。今回はMainActivityの起動時に呼び出すようにしました。

class MainActivity : AppCompatActivity(), DialogFragmentResultListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //インテントオブジェクトを生成
        val intent = Intent(applicationContext, TodoAlarmService::class.java)
        //サービス起動
        startService(intent)

    }
}

サービスは画面がありませんのでスクリーンショットを撮りようがありませんが、通知はこのように表示されます。

サービスの作成

まずはサービスクラスを作成します。

javaフォルダを右クリックして「New」-「Service」-「Service」を選択します。

Class Name(クラス名)を入力します。

Exportedは、外部のアプリから利用可能にするかです。Enabledは登録したサービスを利用可能とするかどうかです。

Finishをクリックするとマニフェストファイルに自動的に登録され、クラスのファイルが作成されます。

package com.example.todomanager

import android.app.Service
import android.content.Intent
import android.os.IBinder

class TodoAlarmService : Service() {

    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }
}

onBindは「サービスのバインド」という方法で実行する場合に必要な処理を記述します。
他のアプリからサービスを呼び出す場合などActivityとServiceを結びつける場合に使用されます。

今回のように「startService」からサービスを起動する場合は使用しませんが、onBindは必ず記述しないといけません。

説明していきます。

class TodoAlarmService : Service() {

    val INTERVAL_PERIOD = 5000

    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }

    override fun onCreate() {
        super.onCreate()
        //サービス起動時の記述
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {

        execAlarm()

        scheduleNextTime(intent)

        return Service.START_STICKY
        //return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        //サービス終了時の記述
    }

    fun execAlarm() {
       //サービスの処理を記述
    }

    fun scheduleNextTime(intent :Intent) {
        //現在のシステム時間を取得
        val now = System.currentTimeMillis()
        //アラーム用のPendingIntentを取得
        val alarmIntent = PendingIntent.getService(this, 0, intent, 0)
        //AlarmManagerを取得
        val am = this.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        //次回サービス起動を予約
        am.set(AlarmManager.RTC, now + INTERVAL_PERIOD, alarmIntent)
    }
}

onCreate、onStartCommand、onDestroy

「onCreate」
サービスが作成されたときに呼び出されます。ここはサービスの初期処理を記述します。

「onStartCommand」
サービスが開始されたときに呼び出されます。ここにサービスで行いたい処理を記述します。

戻り値にサービスが強制終了した場合の振る舞いをIntで返します。

定数名内容
START_NOT_STICKYサービスが強制終了されても自動で再起動しません。
常にサービスが動作する必要がなければ、この定数を返却する方が安全です。
START_STICKYサービスが強制終了された場合に自動で再起動します。
常にサービスを動作させる必要があればこの定数を返却します。
ただし、再起動時Intentはnullになります。
START_REDELIVER_INTENTサービスが強制終了された場合に自動で再起動します。
常にサービスを動作させる必要があればこの定数を返却します。
再起動時Intentは最後に渡されたIntentになります。

「onDestory」
サービスが破棄されるときに呼び出されます。ここは終了処理を記述します。

次回のサービス起動予約をする

サービスを繰り返し実行したい場合は、次回のサービス起動を予約します。

その場合は、AlarmManagerを利用します。INTERVAL_PERIODをあらかじめ設定しておきます。
今回の例でいえばINTERVAL_PERIOD=5000ms(5秒)後にサービスを再実行します。

fun scheduleNextTime(intent :Intent) {
    //現在のシステム時間を取得
    val now = System.currentTimeMillis()
    //アラーム用のPendingIntentを取得
    val alarmIntent = PendingIntent.getService(this, 0, intent, 0)
    //AlarmManagerを取得
    val am = this.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    //次回サービス起動を予約
    am.set(AlarmManager.RTC, now + INTERVAL_PERIOD, alarmIntent)
}

Avticitiyからの呼び出し

サービスを実行します。

//インテントオブジェクトを生成
val intent = Intent(applicationContext, TodoAlarmService::class.java)
//サービス起動
startService(intent)

通知

続いてサービスの中で処理を行い、処理完了後の通知処理を説明します。

通知用のチャンネルの作成

通知をするためには通知チャンネルを生成しなければいけません。これはサービスが作成されたときに行っておきます。

override fun onCreate() {
    super.onCreate()

    //通知チャンネルのID文字列を用意
    val id = NOTIFICATION_ID
    //通知チャンネル名
    val name = "test_notification"
    //通知チャンネルの重要度を標準に設定
    val importance = NotificationManager.IMPORTANCE_DEFAULT
    //通知チャンネルを生成
    val channel = NotificationChannel(id, name, importance)
    //NotificationManagerオブジェクトを取得
    val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    //通知チャンネルを設定
    manager.createNotificationChannel(channel)
}

通知する

処理が完了した時に通知を行います。こちらは処理完了後に実行します。

fun execAlarm() {

    //Notificationを作成するBuilderクラスの生成
    val builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_ID)
    //通知エリアに表示されるアイコンを設定
    builder.setSmallIcon(android.R.drawable.ic_dialog_info)
    //通知ドロワーでの表示タイトルを設定
    builder.setContentTitle("TodoManager")
    //通知ドロワーでの表示メッセージを設定
    builder.setContentText("タイマーです。")
    //起動先Activityクラスを指定したIntentオブジェクトを生成
    val intent = Intent(applicationContext, MainActivity::class.java)
    //起動先Activityに引き継ぎデータを格納
    intent.putExtra("fromNotification", true)
    //PendingIntentオブジェクトを取得
    val stopServiceIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)
    //PendingIntentオブジェクトをビルダーに設定
    builder.setContentIntent(stopServiceIntent)
    //タップされた通知メッセージを自動的に消去するように設定
    builder.setAutoCancel(true)
    //BuilderからNotificationオブジェクトを生成
    val notification = builder.build()
    //NotificationManagerオブジェクトを取得
    val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    //通知
    manager.notify(0, notification)
}

今回は通知からActivityを起動する処理もいれています。通知からのActivityの起動はPendingIntentを使用します。

PendingIntentの」第4引数になっているフラグは以下になります。

定数内容
FLAG_CANCEL_CURRENT既存のPendingIntentがあれば破棄して新しいPendingIntentを返す
FLAG_NO_CREATE既存のPendingInetntがあればそれを使用し、なければnullを返す
FLAG_ONE_SHOT常に最初に作成されたPendingIntentを返す
FLAG_UPDATE_CURRENT既存のPendingIntentがあれば、それは破棄せずextraのデータだけ置き換えて返す

まとめ

今回は、サービスと通知の説明をしました。

サービスは、手動でマニフェストファイルを記載し、クラスを作成してもよいですが、ウィザードを使って生成した方が簡単です。サービスで使用する内容を記述したら呼び出したいActivityからstartService関数で呼び出します。

通知は、通知用のチャンネルを生成しBuilderで作成したオブジェクトを呼び出します。

ここまで読んで頂いてありがとうございます。