Androidでバックグラウンド処理をする場合Serviceを使いますが、その使用についてAndroid8から制限がかかりました。
一番の変更点は”バックグラウンドで処理するならユーザーに通知せよ”という点です。
その対応をしないとすぐにサービスが停止してしまうので、その辺りの対策を踏まえてServiceの使用方法を少し書いてみます。
私はAndroid5で実機テストしながら実装していたので、かなりハマってしまいました。
今回は下記のようなイメージです。
- ActivityからServiceを起動
- ActivityからServiceクラスにアクセス
- Service起動時に通知したNotificationをタップしたらServiceを停止
Service起動はAndroid8以降では、起動から5秒以内にstartForegroundを呼ばないといけない。
Service停止はどこかでBindされていると停止できないので起動時のフラグを間違えないようにする。
などいくつかハマりポイントがありました。
実装例を下記します。
Serviceでは、起動(onStartCommand)で、startForegroundを呼ぶのと、ActivityからServiceを取得させるためにonBindを実装します。
Serviceの起動、停止はローカルにブロードキャストさせます。
public class XxxService extends Service {
public static final String ACTION_START_XXX_SERVICE = "ACTION_START_XXX_SERVICE";
public static final String ACTION_STOP_XXX_SERVICE = "ACTION_STOP_XXX_SERVICE";
private static final String ACTION_CLICK_SERVICE_NOTIFY = "ACTION_CLICK_SERVICE_NOTIFY";
private static final String CHANNEL_XXX_SERVICE = "CHANNEL_XXX_SERVICE";
private LocalBroadcastManager broadcastManager;
private BroadcastReceiver notificationReceiver;
// Activityから接続するためのBinder
public class ServiceLocalBinder extends Binder {
public XxxService getService() {
return XxxService.this;
}
}
private final IBinder binder = new ServiceLocalBinder();
// サービスの開始
public static void start(Context context) {
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo serviceInfo : manager.getRunningServices(Integer.MAX_VALUE)) {
if (XxxService.class.getName().equals(serviceInfo.service.getClassName())) {
// すでに実行中なら起動しない
return;
}
}
Intent intent = new Intent(context, XxxService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
// Android8より前の対応
context.startService(intent);
}
}
@Override
public void onCreate() {
super.onCreate();
broadcastManager = LocalBroadcastManager.getInstance(getApplicationContext());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Android8以降ではNotificationを表示するのにchannnelが必要
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(
CHANNEL_XXX_SERVICE, // チャンネルID
"Xxxサービス", // チャンネル名
NotificationManager.IMPORTANCE_DEFAULT // 重要度
);
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); // ロック画面での表示
manager.createNotificationChannel(channel);
}
notificationReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 通知をタップされたらService終了
if (TextUtils.equals(intent.getAction(), ACTION_CLICK_SERVICE_NOTIFY)) {
XxxService.this.stopSelf();
}
}
};
// Notificationタップを検知するReceiver登録
IntentFilter notificationFilter = new IntentFilter();
notificationFilter.addAction(ACTION_CLICK_SERVICE_NOTIFY);
registerReceiver(notificationReceiver, notificationFilter);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
// サービス開始をブロードキャスト
final Intent startIntent = new Intent();
startIntent.setAction(ACTION_START_XXX_SERVICE);
broadcastManager.sendBroadcast(startIntent);
// Notificationを定義
Notification.Builder builder = new Notification.Builder(this)
.setContentTitle("XxxService")
.setContentText("タップして終了します。")
.setSmallIcon(R.drawable.icon)
.setContentIntent(
// 通知タップ時のPendingIntent
PendingIntent.getBroadcast(
getApplicationContext(),
0 ,
new Intent(ACTION_CLICK_SERVICE_NOTIFY), 0)
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(CHANNEL_XXX_SERVICE);
}
Notification notification = builder.build();
startForeground(1, notification);
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(notificationReceiver);
// サービス停止をブロードキャスト
final Intent intent = new Intent();
intent.setAction(ACTION_STOP_XXX_SERVICE);
broadcastManager.sendBroadcast(intent);
stopForeground(true);
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
Activityでは、ブロードキャストされたServiceの起動、停止に合わせてbind、unbindを行います。
public class XxxActivity extends AppCompatActivity {
// サービスをBindしたか?
private boolean isBound;
// 取得したService
private XxxService xxxService;
// ブロードキャストマネージャ
private LocalBroadcastManager broadcastReceiverManager;
// レシーバ
private BroadcastReceiver serviceStatusReceiver;
private ServiceConnection xxxServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
// IBinderからServiceを取得
xxxService = ((XxxService.ServiceLocalBinder)service).getService();
// XxxServiceを取得した時の何かの処理
// to do something.
}
public void onServiceDisconnected(ComponentName className) {
// サービスとの切断
xxxService = null;
}
};
private void doBindService() {
if (!isBound) {
//サービスと接続
bindService(new Intent(this, XxxService.class), xxxServiceConnection, 0); // ここの0を BIND_AUTO_CREATE にしているとServiceが終了できない。
isBound = true;
}
}
private void doUnbindService() {
if (isBound) {
// コネクションの解除
unbindService(xxxServiceConnection);
isBound = false;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// LocalBroadcastManagerのインスタンスを取得
broadcastReceiverManager = LocalBroadcastManager.getInstance(getApplicationContext());
// レシーバをインスタンス化
serviceStatusReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case XxxService.ACTION_START_XXX_SERVICE:
// サービスをバインド
doBindService();
break;
case XxxService.ACTION_STOP_XXX_SERVICE:
doUnbindService();
break;
}
}
};
final IntentFilter filter = new IntentFilter();
filter.addAction(XxxService.ACTION_START_XXX_SERVICE);
filter.addAction(XxxService.ACTION_STOP_XXX_SERVICE);
broadcastReceiverManager.registerReceiver(serviceStatusReceiver, filter);
// サービス起動
XxxService.start(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
broadcastReceiverManager.unregisterReceiver(serviceStatusReceiver);
doUnbindService();
}
private xxx() {
if (xxxService != null) {
// xxxServiceの何かの処理を実行
// to do something.
}
}
少し長くなりますがバックグラウンド処理が必要なら、仕様上仕方ないというところでしょうか。
ただ、バックグランド処理を通知するということは、ユーザーにとってはバックグラウンドで何が行われているか分かるので良いことかもしれません。
今回はBLEデバイスとの連携のためにServiceが必要で、そのためにNotificationを通知したら、そこからユーザーが停止出来ないとUX的に微妙と思ったのでServiceの停止まで実装してみました。