見出し画像

Androidアプリ開発入門 (7) - サービス

Androidアプリの「サービス」についてまとめました。

・API 29: Android 10 (Q)

前回

1. サービス

サービス」は、バックグラウンドで長時間動作するアプリコンポーネントです。UIはありません。アプリからサービスを起動すると、他のアプリに切り替えても、動作し続けます。

「サービス」には、システムによる「強制終了」と「再起動」があります。メモリ不足時に「強制終了」され、メモリが確保できた時には「再起動」されます。バックグラウンドで長時間実行し続けるほど、強制終了の確率が高くなります。

2. サービスの種類

サービスの種類には、次の2つがあります。

◎ フォアグラウンドサービス
「フォアグラウンドサービス」は、ユーザーが認識できる操作を行うサービスです。音楽を再生するサービスなどが、これにあたります。通知で実行中であることをユーザーに知らせる必要があります。強制終了の確率が低くなります。

◎ バックグラウンドサービス
「バックグラウンドサービス」は、ユーザーに認識されない操作を行うサービスです。ストレージを圧縮するサービスなどが、これにあたります。

◎ バックグラウンドサービスの制限
バックグラウンドサービスの制限は、次のとおりです。

3. サービスの形式

サービスの形式には、次の2つがあります。

◎ スタートサービス
アプリがstartService()でサービス開始を要求した時に、サービスが生成されます。サービスはバックグラウンドで無期限に動作するため、作業が完了したら、アプリのstopService()か、サービス自身のstopSelf()で停止する必要があります。

◎ バインドサービス
アプリがbindService()でサービスにバインドを要求した時に、サービスが生成されます。バインドすると、アプリからサービスを操作することができます。復数のアプリから同時にバインドすることも可能で、アプリのunbindService()で全てアンバインドされた時にサービスは停止します。

バインドを許可するスタートサービスを実装することも可能です。

4. Serviceクラス

「サービス」を作成するには、「Service」を継承したクラスを実装します。

主なメソッドは、次のとおりです。

◎ onStartCommand()
アプリが「サービス開始」(startService())した時に呼ばれるメソッドです。「バインド」のみ提供する場合は、このメソッドを実装する必要はありません。

◎ onBind()

アプリがサービスに「バインド」(bindService())した時に呼ばれるメソッドです。バインドを提供する場合は「IBinder」を返し、提供しない場合は nullを返します。

◎ onCreate()

サービス生成時に呼ばれるメソッドで、サービスの前処理を実装します。onStartCommand()やonBind()が呼ばれる前に呼ばれます。

◎ onDestroy()

サービス破棄時に呼ばれるメソッドで、サービスの後処理を実装します。

◎ サービスの再起動方法
onStartCommand()の戻り値で、メモリ不足によって処理完了前にサービスが停止された後、十分メモリ確保できた場合の、サービスの再起動方法を指定します。

・START_STICKY : サービスを再起動し、nullのIntentで再度onStartCommand()を呼び出す。
・START_NOT_STICKY : サービスを再起動しない。
・START_REDELIVER_INTENT : サービスを再起動し、前回と同じIntentで再度onStartCommand()を呼び出す。

5. フォアグラウンドサービスの実装

10秒かかる処理(スリープで代用)を実行するフォアグラウンドサービスを作ります。フォアグラウンドサービスは、通知で実行中であることをユーザーに知らせる必要があります。

◎ 通知パッケージのインストール
モジュールの「build.gradle」に以下の参照を追加します。

implementation "com.android.support:support-compat:28.0.0"

◎ AndroidManifest.xml
「AndroidManifest.xml」に以下の項目を設定します。

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

サービスの定義も追加します。

<service android:name=".MyForegroundService" android:exported="false" />

◎ UI
今回は、「Button」(id@button)を1つ配置します。

画像1

◎ コードの実装

package net.npaka.serviceex
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button

class MainActivity : AppCompatActivity(), View.OnClickListener {
    private var button: Button? = null

    // 起動時に呼ばれる
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 参照
        this.button = findViewById(R.id.button)
        this.button!!.setOnClickListener(this)
        this.button!!.text = "フォアグラウンドサービスの開始"        
    }

    // クリック時に呼ばれる
    override fun onClick(v: View) {
        // フォアグラウンドサービスの開始
        if (!MyForegroundService.isRunning) {
            val intent = Intent(this, MyForegroundService::class.java)
            startForegroundService(intent)
        }
    }
}
package net.npaka.serviceex
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder

class MyForegroundService : Service() {
    companion object {
        var isRunning = false
    }

//====================
// ライフサイクル
//====================
    // サービスの開始
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        MyForegroundService.isRunning = true        
        
        // 通知チャンネルの生成
        createNotificationChannel()

        // フォアグラウンドの開始
        val notification = createNotification("MyForegroundService実行中")
        startForeground(1234, notification)

        Thread(
            Runnable {
                // 10秒かかる処理 (スリープで代用)
                for (i in 0..10) {
                    if (!MyForegroundService.isRunning) return@Runnable
                    Thread.sleep(1000)
                }

                // フォアグラウンドの停止
                stopForeground(Service.STOP_FOREGROUND_REMOVE)

                // サービスの停止
                stopSelf()
            }).start()
        return START_STICKY
    }

    // バインド
    override fun onBind(intent: Intent): IBinder? {
        return null
    }
    
    // サービスの停止
    override fun onDestroy() {
        super.onDestroy()
        MyForegroundService.isRunning = false
    }    


//====================
// 通知
//====================
    // 通知チャンネルの生成
    private fun createNotificationChannel() {
        // 通知チャンネルの生成
        val channel = NotificationChannel(
            "TEST_CHANNEL_ID", // チャンネルID
            "Test", // チャンネル名
            NotificationManager.IMPORTANCE_DEFAULT) // 重要度

        // システムに通知チャンネルを登録
        val notificationManager: NotificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
    } 

    // 通知の生成
    private fun createNotification(text: String) : Notification {
        return Notification.Builder(this, "TEST_CHANNEL_ID")
            .setSmallIcon(R.drawable.ic_launcher_foreground) // 小アイコン
            .setContentText(text) // テキスト
            .build()
    }
}
◎ startForegroundService()
アプリからフォアグラウンドサービスを開始したい場合に実行します。このメソッドが作成するのはバックグラウンドサービスですが、サービス自身がフォアグラウンドを開始することをシステムに通知します。

◎ startForeground()
サービス内でフォアグラウンドを開始します。サービスが作成されたら、5秒以内に呼ぶ必要があります。

◎ stopForeground()
サービス内でフォアグラウンドを停止します。
・STOP_FOREGROUND_REMOVE : フォアグラウンドの通知を削除。
・STOP_FOREGROUND_DETACH : フォアグラウンドの通知を残す。

◎ startService()
サービス内で自身を停止します。

6. バックグラウンドサービスの実装

10秒かかる処理(スリープで代用)を実行するバックグラウンドサービスを作ります。バックグラウンドサービスは、通知で実行中であることをユーザーに知らせる必要はありませんがテスト用に表示しています。

◎ 通知パッケージのインストール
モジュールの「build.gradle」に以下の参照を追加します。

implementation "com.android.support:support-compat:28.0.0"

◎ AndroidManifest.xml
サービスの定義を追加します。

<service android:name=".MyBackgroundService" android:exported="false" />

◎ UI
今回は、「Button」(id@button)を1つ配置します。

画像2

◎ コードの実装

package net.npaka.serviceex
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button

class MainActivity : AppCompatActivity(), View.OnClickListener {
    private var button: Button? = null

    // 起動時に呼ばれる
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 参照
        this.button = findViewById(R.id.button)
        this.button!!.setOnClickListener(this)
        this.button!!.text = "バックグラウンドサービスの開始"        
    }

    // クリック時に呼ばれる
    override fun onClick(v: View) {
        // バックグラウンドサービスの開始
        if (!MyBackgroundService.isRunning) {
            val intent = Intent(this, MyBackgroundService::class.java)
            startService(intent)
        }
    }
}
package net.npaka.serviceex
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import androidx.core.app.NotificationManagerCompat

class MyBackgroundService : Service() {
    companion object {
        var isRunning = false
    }

//====================
// ライフサイクル
//====================
    // サービスの開始
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        MyBackgroundService.isRunning = true

        // 通知チャンネルの生成
        createNotificationChannel()

        // 通知の送信
        sendNotification("MyBackgroundService実行中", 1234)

        Thread(
            Runnable {
                // 10秒かかる処理 (スリープで代用)
                for (i in 0..10) {
                    if (!MyBackgroundService.isRunning) return@Runnable
                    Thread.sleep(1000)
                }

                // 通知のキャンセル
                cancelNotification(1234);

                // サービスの停止
                stopSelf()
            }).start()
        return START_STICKY
    }

    // バインド
    override fun onBind(intent: Intent): IBinder? {
        return null
    }

    // サービスの停止
    override fun onDestroy() {
        super.onDestroy()
        MyBackgroundService.isRunning = false
    }


//====================
// 通知
//====================
    // 通知チャンネルの生成
    private fun createNotificationChannel() {
        // 通知チャンネルの生成
        val channel = NotificationChannel(
            "TEST_CHANNEL_ID", // チャンネルID
            "Test", // チャンネル名
            NotificationManager.IMPORTANCE_DEFAULT) // 重要度

        // システムに通知チャンネルを登録
        val notificationManager: NotificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
    }

    // 通知の送信
    private fun sendNotification(text: String, notifId: Int) {
        // 通知の生成
        val notification: Notification = Notification.Builder(this, "TEST_CHANNEL_ID")
            .setSmallIcon(R.drawable.ic_launcher_foreground) // 小アイコン
            .setContentText(text) // テキスト
            .build()

        // 通知の送信
        with (NotificationManagerCompat.from(this)) {
            notify(notifId, notification) // 通知ID (更新、削除で利用)
        }
    }

    // 通知のキャンセル
    private fun cancelNotification(notifId: Int) {
        // 通知のキャンセル
        with (NotificationManagerCompat.from(this)) {
            cancel(notifId)
        }
    }
}
◎ startService()
フォアグラウンドアプリからバックグラウンドサービスを開始します。

◎ startService()
サービス内で自身を停止します。

7. バインドサービスの実装

乱数を取得するバインドサービスを作ります。

◎ AndroidManifest.xml
サービスの定義を追加します。

<service android:name=".MyBindService" android:exported="false" />

◎ UI
今回は、「Button」(id@button)を1つ配置します。

画像2

◎ コードの実装

package net.npaka.serviceex
import android.app.Service
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.IBinder
import android.view.View
import android.widget.Button
import android.widget.Toast

class MainActivity : AppCompatActivity(), View.OnClickListener {
    private var button: Button? = null

    // バインド
    private var myBindService: MyBindService? = null
    private var bound: Boolean = false

    // 起動時に呼ばれる
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 参照
        this.button = findViewById(R.id.button)
        this.button!!.setOnClickListener(this)
        this.button!!.text = "バインドサービスの開始"
    }

    // 開始時に呼ばれる
    override fun onStart() {
        super.onStart()

        // バインドサービスのバインド
        val intent = Intent(this, MyBindService::class.java)
        bindService(intent, connection, Service.BIND_AUTO_CREATE)
    }

    // 停止時に呼ばれる
    override fun onStop() {
        super.onStop()
        unbindService(connection)
        this.bound = false
    }

    // クリック時に呼ばれる
    override fun onClick(v: View) {
        if (this.bound) {
            val num: Int = this.myBindService!!.randomNumber()
            Toast.makeText(this, "number: "+num, Toast.LENGTH_SHORT).show()
        }
    }

    // サービス接続リスナー
    private val connection: ServiceConnection = object : ServiceConnection {
        // サービス接続時に呼ばれる
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            val binder = service as MyBindService.MyBinder
            myBindService = binder.getService()
            bound = true
        }

        // サービス切断時に呼ばれる
        override fun onServiceDisconnected(name: ComponentName) {
            bound = false
        }
    }
}
package net.npaka.serviceex
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.Binder
import kotlin.random.Random

class MyBindService : Service() {
    // バインダー
    private val binder = MyBinder()
    inner class MyBinder : Binder() {
        fun getService(): MyBindService = this@MyBindService
    }

//====================
// ライフサイクル
//====================
    // バインド
    override fun onBind(intent: Intent): IBinder? {
        return binder
    }

    // 乱数の取得
    fun randomNumber(): Int {
        return Random.nextInt(100)
    }
}

【おまけ】 Intentのパラメータの確認

Intentのパラメータを確認するコードは、次のとおりです。

val extras = intent!!.extras
for (key in extras!!.keySet()) {
    val obj = extras!![key]
    Log.d("debug", key+":"+obj)
}

次回



この記事が気に入ったらサポートをしてみませんか?