Servce はバックグラウンドで作業をさせたい場合に使います。システムは直ぐにスリープに入れてしまいます。それでも裏で色々やりたい場合に有効です。例えば音楽の再生など
2024.1.1
Service
Serviceがバックグラウンドで動作するので、非同期と勘違いしそうですがそうではなく、Activityから表示UIを無くしたようなコンポーネントです。
サービスは、そのホスティング プロセスのメインスレッドで実行され、サービスが自身のスレッドを作成することはなく、別のプロセスで実行されることもありません
CPU を集中的に使う作業、Audioの再生やネットワーク作業など、を行う場合は、サービス内に新しいスレッドを作成してその作業を行う必要があります
Serviceの開始
Serviceを開始するには、
- startService()
- 開始後は、開始したActivityが破棄されても基本的には実行し続けられる、Activityとは別の独自のContextを持っている
- 呼び出し側からの制御は開始か停止させるのみで、タスクの終了をコールバックしないので自身で終了するなどの手当が必要
- bindService()
- 要求を送信したり、結果を取得したりとServiceを制御できますが、呼び出し元のActivityが終了すると一緒に終了してしまう
これにAndroid 8.0からの追加として、
startForegroundService()
を使ってバックグラウンド実行制限をある程度回避できます。
ForegroundServicehは、
- API レベル 29 以降:
location
サービスタイプを使用して、位置情報を使用するすべてのフォアグラウンド サービスを宣言する必要があります。 - API レベル 30 以降: カメラまたはマイクを使用するすべてのフォアグラウンド サービスを、それぞれ
camera
またはmicrophone
サービスタイプを使用して宣言する必要があります。 - API レベル 34 以降: すべてのフォアグラウンド サービスを、そのサービスタイプとともに宣言する必要があります。
ということで、相変わらず変更が多いです
startService()
Activity からServiceを呼び出すのはIntent をセットして、startService(intent) で行います。
1 2 |
Intent intent = new Intent(getApplication(), TestService.class); startService(intent); |
Activity から Activity に遷移させる場合は
startActivity(intent)でしたので、startService(intent)に変わっただけとわかりやすです
一方、サービス側は
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class TestService extends Service { @Override public void onCreate() { super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // to do something return START_NOT_STICKY; } @Override public void onDestroy() { super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return null; } } |
Service を継承して
- onCreate()
- これはActivity同様、最初だけ呼ばれて終わりなので初期化だけです。
- onStartCommand()
- サービスで実行させたいコードはここに記述
- 戻り値は:
- START_NOT_STICY:強制終了しても再起動しない
- START_STICKY:強制終了されても自動的にサービスが再起動、最後のインテントは再配信しない
- START_REDELIVER_INTENT:強制終了されても自動的にサービスが再起動、サービスに最後に配信されたインテントで onStartCommand() を呼び出す
- onDestroy()
- onBind()
- bindService() で呼び出した場合
onStartCommand() ではなく
onBind() がcallbackされます
- bindService() で呼び出した場合
Manifest にサービスクラスの定義を忘れないように
パーミッション
サービスタイプの指定
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <application ... <activity ... </activity> <service android:name=".TestService" android:foregroundServiceType="mediaPlayback" android:exported="false" /> </application> </manifest> |
Serviceの終了
startService()で始めたServiceは勝手には止まりません。
MainActivityからstopService(intent)で止める
1 2 3 |
Intent intent = new Intent(getApplication(), TestService.class); // Serviceの停止 stopService(intent); |
ミュージック再生などのタスクが終了したのを受けてstopSelf()を実行することもできます。
1 |
stopSelf(); |
Oreoからバックグラウンドに対する制限ができて
アプリがバックグラウンドに移行すると数分間のウィンドウが提供され、アプリはそのウィンドウ内でサービスを作成して使用できます。
そのウィンドウの終了時に、アプリはアイドル状態であると見なされます。 システムはこの時点で、アプリがサービスのService.stopSelf()
メソッドを呼び出したかのように、アプリのバックグラウンド サービスを停止します。
バックグラウンド実行制限 | Android Developers
またマルチタスクメニューからアプリをユーザーが終了させることもあります。
startForegroundService()
バックグラウンドでアプリが実行できないと、一番困るのはmusic系のアプリでしょうか、それについては
バックグラウンド実行制限 | Android Developersにはこのような一文があります。
Android 8.0 では、追加機能があります。システムは、バックグラウンド アプリによるバッグラウンド サービスの作成を許可しません。
そのため、Android 8.0 では、フォアグラウンドで新しいサービスを作成する Context.startForegroundService() メソッドが新たに導入されています。システムによってサービスが作成されると、アプリは、サービスの startForeground() メソッドを 5 秒以内に呼び出して、
その新しいサービスの通知をユーザーに表示します。
アプリが startForeground() を制限時間内に呼び出さない場合、サービスが停止し、アプリが ANR になります。
ということでこれを使ってMediaPlayerでをServiceで音楽再生してみます。
また、フォアグラウンド サービスタイプの設定が必要です
サンプルコード
ActivityからServiceを開始し、Service内で5秒以内にstartForegroundを呼び出します。
これでステータスバーにはアイコンが表示されてForgroundにいるかのようになります
マニュフェストには、音楽再生なのでMediaPlaybackとして
- FOREGROUND_SERVICEのpermission設定とサービスタイプのpermission
- <service … />の追加とサービスタイプの指定
- android:foregroundServiceType=”mediaPlayback”
- バックグラウンドに入った後で、通知からActivityを起動させるための<activity>設定
- android:taskAffinity=””
- android:excludeFromRecents=”true”
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <application ... <activity ... android:taskAffinity="" android:excludeFromRecents="true" ... </activity> <service android:name=".TestService" android:foregroundServiceType="mediaPlayback" android:exported="false" /> </application> </manifest> |
MainActivityではServiceの開始とストップのためのボタンを設置
MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package com.example.testserviceapp; import android.os.Bundle; import androidx.activity.EdgeToEdge; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import android.content.Intent; import android.widget.Button; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_main); ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); return insets; }); Button buttonStart = findViewById(R.id.button_start); buttonStart.setOnClickListener( v -> { Intent intent = new Intent(getApplication(), TestService.class); // Serviceの開始 startService(intent); }); Button buttonStop = findViewById(R.id.button_stop); buttonStop.setOnClickListener(nv -> { Intent intent = new Intent(getApplication(), TestService.class); // Serviceの停止 stopService(intent); }); } } |
サービス側では、MediaPlayerを使ってmp3を再生させてみます。
MediaPlayerについてはこちらで解説しています。
Rawフォルダーを作成して適当なmp3の音楽ファイルを入れておきます。
以下のJavaクラスを作成しコーディング
TestService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
package com.example.testserviceapp; import androidx.annotation.Nullable; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.media.MediaPlayer; import android.os.IBinder; import android.util.Log; import android.widget.Toast; public class TestService extends Service { private MediaPlayer mediaPlayer; private int counter = 0; @Override public void onCreate() { super.onCreate(); Log.d("debug", "onCreate()"); // ..\res\raw\sample.mp3 mediaPlayer = MediaPlayer.create(this, R.raw.sample); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d("debug", "onStartCommand()"); Context context = getApplicationContext(); String channelId = "default"; String title = context.getString(R.string.app_name); // 通知からActivityを起動できるようにする Intent notifyIntent = new Intent(this, MainActivity.class); // Set the Activity to start in a new, empty task notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity( this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); // Notification Channel 設定 NotificationChannel channel = new NotificationChannel( channelId, title , NotificationManager.IMPORTANCE_DEFAULT); if(notificationManager != null){ notificationManager.createNotificationChannel(channel); Notification notification = new Notification.Builder(context, channelId) .setContentTitle(title) // android標準アイコンから .setSmallIcon(android.R.drawable.ic_media_play) .setContentText("MediaPlay") .setAutoCancel(true) .setContentIntent(pendingIntent) .setWhen(System.currentTimeMillis()) .build(); // startForeground startForeground(1, notification); audioStart(); } return START_NOT_STICKY; //return START_STICKY; //return START_REDELIVER_INTENT; } private void audioStart(){ counter++; Log.d("debug","audioStart: "+counter); if(mediaPlayer != null){ // ループ mediaPlayer.setLooping(true); // ループしない //mediaPlayer.setLooping(false); // 再生する mediaPlayer.start(); // トースト String str ="Start Walking\n(c)Music-Note.jp"; Toast toast = Toast.makeText(this, str, Toast.LENGTH_LONG); toast.show(); // 終了を検知するリスナー mediaPlayer.setOnCompletionListener(mp -> { Log.d("debug","end of audio"); audioStop(); // Service終了 stopSelf(); }); } } private void audioStop() { // 再生終了 mediaPlayer.stop(); // リセット mediaPlayer.reset(); // リソースの解放 mediaPlayer.release(); mediaPlayer = null; } @Override public void onDestroy() { super.onDestroy(); Log.d("debug", "onDestroy()"); if(mediaPlayer != null){ Log.d("debug","end of audio"); audioStop(); } // Service終了 stopSelf(); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } } |
以下にレイアウトやリソースがあります
レイアウトです。簡単にするためLinearLayoutを使いました。
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/button_start" android:text="@string/start" android:layout_margin="40dp" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.4" /> <Button android:id="@+id/button_stop" android:text="@string/stop" android:layout_margin="40dp" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.6" /> </androidx.constraintlayout.widget.ConstraintLayout> |
strings.xml
1 2 3 4 5 |
<resources> <string name="app_name">YourAppName</string> <string name="start">Start Service</string> <string name="stop">Stop Service</string> </resources> |
これでアプリを終了、マルチタスクメニューから削除してもmp3が再生されています。
フォアグラウンドサービスはAPIによって変更が多々あるので確認が必要です
関連ページ:
References:
サービス | Android Developers
バックグラウンド実行制限 | Android Developers
Service | Android Developers