やりたいこと
ボタンを押すと、フォアグラウンドサービスを起動するやり方を記録しておきます。
環境
OS : Windwos10
Android Studio 4.0
言語 : java
参考にしたサイト
■Foreground Serviceの基本
https://qiita.com/naoi/items/03e76d10948fe0d45597
おおむねこちらのページを参考にしてます。
■公式情報
https://developer.android.com/guide/components/services?hl=ja
公式情報も参考にしました。
背景
※ここは読み飛ばしていただいて結構です。※
充電ケーブルが接続されたかどうかを検知して、何か処理するというアプリを検討中なのですが、通常のアプリだと裏側にいると勝手にOSによって終了させられてしまいます。
Androidの公式ページでこのように記載されてます。
「システムは RAM を解放する必要がある場合にプロセスを強制終了します。システムが特定のプロセスを強制終了する可能性は、...」
https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ja
また、Android7まではマニフェストに「イベントを検知するよ。」という一文を書いておくと、アプリのプロセスがいなくても、イベントが発生すると再起動してくれたのですが、Android8以降はアプリのプロセスがいないと、イベントがひろえなくなってしまいました。
(イベントを検知する仕組みの詳細は
こちらを参考にしてください。)
じゃあどうするかというと、アプリをServiceとして裏側で生かしておいて、常にイベントを検知できるようすればよいということになります。
それで終わりかというと、Android8以降ではまたルール改正が発生しており、Serviceをバックグラウンド(通知とかなにもださずに)で動作させていると、強制終了させられるというルールが追加されました。
詳しくは
こちらを参考にしてください。
ルール改正がはげしい...
じゃあどうするかというと、その答えがForegroundServiceとなります。
ということで、前置きが長くなりましたがForegroundServiceの勉強をしたので、メモしておきます。
ざっくり言うと
ざっくりいうと以下の手順となります。
・serviceクラス作成
この中で自分がForegroundであることを知らせるための処理をごちゃごちゃいれる。
・通知用のアイコン準備
・Manifestにserviceクラスを登録
・MainActivityからserviceを起動
それでは、細かく書いていきます。
Step1.準備
1.Android StudioでEmpty Activityでプロジェクトを作成する。
Name : MyApp
Package name : test.test ← 適当につけました。
Minimum SDK : Api 16 (android 4.1) ←デフォルトのまま
2.ボタンを1個配置して、OnClickListenerを作る。
後でこのボタンを押すとServiceが起動するようにします。
大事なところはまだですが、ここまではこんな感じのコードになります。
package test.test;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ボタンのイベント
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// サービスを呼ぶ
}
});
}
}
Step2.通知用のアイコン作成
serviceクラスから通知(notification)を行う必要があるのですが、通知用の小さいアイコンが必須になるので、事前に作っておきます。
(1)app - res - drawable - 右クリック - New - Image Assetを選択
(2)下図の①のところをNotification Iconsに設定、
②のところに任意の名称を設定(ここではic_service_notificationに設定しました)、
③でiconの形状を設定(ここではデフォルトのままにしました。)します。
(3)設定したらNext - Finish
Step3.serviceクラス作成
package test.test;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
//(1) Serviceの派生クラスを作成
public class MyService extends Service {
//(2) 必須のメソッド。 バインドしない場合はnullを返す。
@Override
public IBinder onBind(Intent intent){
return null;
}
//(3.3)の一部 startforegroudnに渡すIDを定義する。 0以外の数字 。
private static final int ONGOING_NOTIFICATION_ID = 1;
//(3) 呼び出し側でstartService()かstartForegroundService() をすると呼び出されます。
@Override
public int onStartCommand(Intent intent,
int flags,
int startId) {
//(3.1) チャンネルIDの登録
String channelID = "MY_CHANNEL_ID"; // 通知チャンネル用のID。適当な名前を定義
createNotificationChannel(channelID);
// 通知をタップしたときにアプリを呼び出す準備
Intent notificationIntent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
// 通知の準備
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelID)
.setContentTitle("通知のタイトル")
.setContentText("通知の内容")
.setSmallIcon(R.drawable.ic_service_notification)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
// (3.3) このAPIを呼ぶと、フォアグラウンドサービスとなる。
startForeground(ONGOING_NOTIFICATION_ID, builder.build());
// (3.4) 戻り値を指定。
return START_STICKY;
}
// 通知のチャンネルを作成する。この関数はほぼもってきたそのまま。
// https://developer.android.com/training/notify-user/build-notification?hl=ja
private void createNotificationChannel(String channelID) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//アプリ - 通知 に表示される情報
CharSequence name = "サービス起動中の通知"; // なんでもOK。
String description = "サービス起動中の通知は、XXXのための通知です。"; // なんでもOK。
// 通知のレベルとか名称とか設定
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(channelID, name, importance);
channel.setDescription(description);
// 通知の登録
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
}
上記コードの説明
(1)serviceクラスを継承したクラスを作成します。
(2)onBind()を実装。必須のメソッドとなります。
bindしない場合はreturn nullでOKです。
(3)onStartCommand()を実装。
親アプリがstartService()をするとこちらが呼ばれます。
(3.1) createNotificationChannelをして、通知チャンネルを作成。Android8以降で必要。
設定 - 通知で使用される機能になります。
(3.2) 通知の準備
フォアグラウンドサービスにするには、通知を表示して、生きていることをユーザーに知らせる必要があるため、必須となります。
その準備として、通知のタイトルや通知のアイコンなどを準備してます。
(3.3) startForeground()を実行
このAPIを呼び出すことでフォアグラウンドサービスとなります。第一引数のidには0以外 の値を設定する必要があります。ここでは1を指定しました。
(3.4) 戻り値を設定( 3種類あるので用途に合わせて設定 )
このコードでは文字列は直値を記載してますが、実際はstrings.xmlに定義して、使用したほうが良いと思います。
Step4.manifest
(1) applicationの中に、<service android:name=".MyService">を追加します。
(2) application>の外に、<uses-permission android:name="android.permission.FOREGROUND_SERVICE">を追加します。
(2)のほうは、Android9以降では必須となります。
以下xml全体となります。うまくコードをはれなかったので、図を張ってます。
Step5.MainActivityからサービスを呼ぶ
package test.test;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ボタンのイベント
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// サービスを呼ぶ
Intent serviceIntent = new Intent(MainActivity.this, MyService.class);
startService(serviceIntent);
}
});
}
}
Step5.テスト
emulatorの5.1と10で動くことだけ確認しました。