Android でバックグラウンド(Service)処理(Android8対応)

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の停止まで実装してみました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA