2020年7月23日木曜日

Android Foreground Serviceのメモ

やりたいこと
ボタンを押すと、フォアグラウンドサービスを起動するやり方を記録しておきます。

環境
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で動くことだけ確認しました。
0 件のコメント:
コメントを投稿