Androidのフォアグラウンドサービスで位置情報を取得する

Androidのフォアグラウンドサービスを使ってバックグラウンドで位置情報を取得します。
最初に伝えておきますが、あくまで個人用途での使用であり、自身の散策履歴を保存することを想定しています。
最終的には、以下の構成を目指します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/2f3f2bc0-4a07-da23-a52d-e407a67484ef.png)

今回は、左下のAndroidアプリケーションの部分の説明です。

とりあえずソースコード類は以下のGitHubに上げておきました。

poruruba/LocationRecorder

https://github.com/poruruba/LocationRecorder

(参考）
[Androidのフォアグラウンドサービスで位置情報を取得する](https://qiita.com/poruruba/items/aa02207080a390e1ac85)
[CasaOSで自分NASを構築する](https://qiita.com/poruruba/items/03f7179c9e370bfdd5ec)
[Nginx Proxy Managerを使ってワイルドカードSSL証明書を作成する](https://qiita.com/poruruba/items/13ec1d93b27a43359ea5)
[ThingsBoardをDockerでインストール](https://qiita.com/poruruba/items/7184e829f9b2dee2a776)

それ以外の構成要素について簡単に説明します。

* Ubuntu
Linuxディストリビューションです。後述するソフトウェアを動かすためのサーバPC用のOSです。

https://jp.ubuntu.com/

* CasaOS
Linuxサーバにインストールして、NASのようにGUIベースで管理するためのソフトウェアです。Dockerが統合されているので、さまざまなアプリを管理できます。

https://casaos.io/

* ThingsBoard
IoTを扱うためのプラットフォームです。
取得したAndroid端末の位置情報を保存・可視化するために使います。

https://thingsboard.io/

* Nginx Proxy Manager
NginxのリバースプロキシをGUIで管理するためのソフトウェアです。
ThingsBoardをHTTPS化したり、外部からアクセスできるようにします。

https://nginxproxymanager.com/

# 位置情報をバックグラウンドで取得するAndroidアプリケーション

以下の処理が必要です。

* ユーザから位置情報取得権限の許可を得る
* バックグラウンドで位置情報を取得するためには、Androidのフォアグラウンドサービスを使う必要がある
* 位置情報の取得には、FusedLocationProviderClientを使う

順番に説明します。

# ユーザから位置情報取得権限の許可

AndroidManifest.xmlに以下を記載しておきます。

```xml:android\LocationRecorder\app\src\main\AndroidManifest.xml
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
```

ユーザからACCESS_FINE_LOCATIONの権限を取得していなければ、リクエストします。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\MainActivity.java
    private void checkLocationPermissions() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_PERMISSION_LOCATION);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode == REQUEST_PERMISSION_LOCATION) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            } else {
                Toast.makeText(this, "位置情報の許可がないので計測できません", Toast.LENGTH_LONG).show();
            }
        }
    }
```

# フォアグラウンドサービスと位置情報取得

位置情報取得のためのFusedLocationProviderClientを利用するため、以下をappのbuild.gradleに追記し、Android Studio上で「Sync Project with Gradle Files」ボタンをクリックします。

```gradle:android\LocationRecorder\app\build.gradle
    implementation 'com.google.android.gms:play-services-location:21.0.1'
```

また、フォアグラウンドサービスの実行のためにAndroidManifest.xmlに以下を追加します。

```xml:android\LocationRecorder\app\src\main\AndroidManifest.xml
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
```

フォアグラウンドサービスは、以下のようにして起動します。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\MainActivity.java
            Intent intent = new Intent(this, LocationService.class);
            startForegroundService(intent);
```

引数に、フォアグラウンドサービスとして自作したクラス「LocationService」を指定します。
AndroidManifest.xmlにも以下を追加します。

```xml:android\LocationRecorder\app\src\main\AndroidManifest.xml
        <service
            android:name=".LocationService"
            android:parentActivityName=".MainActivity"
            android:foregroundServiceType="location"/>
```

このとき、位置情報取得を目的にフォアグラウンドサービスを立ち上げることを示すため、```android:foregroundServiceType="location"```を付けています。

自作のLocationServiceには、Serviceを継承します。そのため、以下を実装する必要があります。

*    @Override
    public void onCreate() {

この自作のクラスがインスタンス化されたときに呼び出されます。今回は、NotificationManagerの取得と、FusedLocationProviderClientを取得しています。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\LocationService.java
        notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        if( notificationManager == null ) {
            Log.d(MainActivity.TAG, "NotificationManager not available");
            return;
        }

        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
        if( fusedLocationClient == null ) {
            Log.d(MainActivity.TAG, "fusedLocationClient not available");
            return;
        }
```

ちなみに、NotificationManagerは、フォアグラウンドサービスを立ち上げるときには通知を表示しなければいけないために、通知の設定に使います。

*    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

メインアクティビティからstartForegroundServiceで自作のフォアグラウンドサービスが開始されたときに呼び出されます。
この中では、通知を設定してから、位置情報の取得を開始します。

通知設定は以下の通りです。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\LocationService.java
            Intent notifyIntent = new Intent(this, MainActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, NOTIFICATION_TITLE , NotificationManager.IMPORTANCE_DEFAULT);
            notificationManager.createNotificationChannel(channel);
            Notification notification = new Notification.Builder(context, CHANNEL_ID)
                    .setContentTitle(NOTIFICATION_TITLE)
                    .setSmallIcon(android.R.drawable.btn_star)
                    .setAutoCancel(false)
                    .setShowWhen(true)
                    .setWhen(System.currentTimeMillis())
                    .setContentIntent(pendingIntent)
                    .build();
            startForeground(NOTIFICATION_ID, notification);
```

表示される通知をタップしたら、自分自身のメインアクティビティが起動するように、PendingIntentも指定しています。
また、複数のメインアクティビティが起動するのは不都合なので、AndroidManifest.xmlのMainActivityに```android:launchMode="singleTask"```を指定しておきます。

位置情報の取得は以下の通りです。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\LocationService.java
    private void startGPS() {
        if (fusedLocationClient != null) {
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
                    ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
                return;

            int minTime = pref.getInt("minTime", DefaultMinTime);
            float minDistance = pref.getFloat("minDistance", DefaultMinDistance);

            LocationRequest locationRequest = new LocationRequest.Builder(minTime)
                    .setMinUpdateIntervalMillis(minTime)
                    .setMinUpdateDistanceMeters(minDistance)
                    .build();
            fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback = new LocationCallback() {
                @Override
                public void onLocationResult(LocationResult locationResult) {
                    super.onLocationResult(locationResult);
                    Log.d(MainActivity.TAG, "onLocationResult");

                    Location location = locationResult.getLastLocation();

                    // Locationに緯度経度が入っています。
                }
            }, Looper.getMainLooper());
        }
    }
```

まずは、フォアグラウンドサービスでも、ACCESS_FINE_LOCATIONの権限を取得しているかを確認します。

fusedLocationClient.requestLocationUpdatesを呼び出しておけば、定期的にコールバックで位置情報を取得されるようになります。
その前に、LocationRequestを生成して引数に渡しています。MinUpdateIntervalMillisは、この時間より早くは位置情報を取得しないようにし、MinUpdateDistanceMetersは、この距離以上移動しなければ、位置情報を取得しないようにします。

コールバック関数の中で、Locationを取得した後の処理は任意ですが、今回はAndroid内のSQLiteデータベースにいったん保存して、たまってからThingsBoardへの位置情報のアップロードをしています。詳細はまた今度の投稿で紹介します。

*    @Override
    public void onDestroy() {

フォアグラウンドサービスが停止されるときに呼び出されます。
以下のように後始末をしています。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\LocationService.java
        if( locationCallback != null ){
            fusedLocationClient.removeLocationUpdates(locationCallback);
            locationCallback = null;
        }
```

*    @Override
    public IBinder onBind(Intent intent) {

今回は特に利用していません。

# メインアクティビティとフォアグラウンドサービスの連携

メインアクティビティとフォアグラウンドサービスで値を共有したい場合があります。
その場合は、SharedPreferenceを使っています。
メインアクティビティとフォアグラウンドサービスそれぞれで以下のようにインスタンスを取得して使っています。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\MainActivity.java
pref = getSharedPreferences("Private", MODE_PRIVATE);
```

また、フォアグラウンドサービスからメインアクティビティへの任意データの通知にブロードキャストを使っています。
送信する側は以下のようにしてブロードキャストします。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\LocationService.java
                    Intent broadcastIntent = new Intent();
                    broadcastIntent.setAction(LocationUpdateAction);
                    broadcastIntent.putExtra("datetime", item.datetime);
                    broadcastIntent.putExtra("lat", item.lat);
                    broadcastIntent.putExtra("lng", item.lng);
                    broadcastIntent.putExtra("speed", item.speed);
                    sendBroadcast(broadcastIntent);
```

受ける側のメインアクティビティでの処理です。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\MainActivity.java
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(LocationService.LocationUpdateAction);
        registerReceiver(mReceiver, intentFilter);
```

これで、送信側が指定したLocationService.LocationUpdateAction で示されるアクション名で待ち受け、受け取るとmReceiverが呼び出されます。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\MainActivity.java
    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d(TAG, "onReceive");

            Bundle bundle = intent.getExtras();
            long datetime = bundle.getLong("datetime");
            double lat = bundle.getDouble("lat");
            double lng = bundle.getDouble("lng");
            float speed = bundle.getFloat("speed");
            LocationDbHelper.LocationItem item = new LocationDbHelper.LocationItem(datetime, lat, lng, speed);
            handler.sendUIMessage(UIHandler.MSG_ID_OBJECT, item);
        }
    };
```

onReceiveの中では、画面の操作はできないので、Handlerクラスを介して操作しています。

# 環境に合わせて変更

セットアップされた環境に合わせて以下を編集する必要があります。
今後の投稿で取り上げるThingsBoardに関する設定です。

フォアグラウンドサービス「LocationService」
実際に立ち上げる予定のThingsBoardのURLです。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\LocationService.java
    static final String target_url_base = "https://【ThingsBoardサーバのホスト名】.poruru.work/api/v1/";
```

メインアクティビティ「MainActivity」
ThingsBoardに登録したデバイスのアクセストークンです。

```java:android\LocationRecorder\app\src\main\java\jp\or\myhome\sample\locationrecorder\MainActivity.java
    private static final String DefaultAccessToken = "【ThingsBoardのアクセストークン】";
```

# 参考

https://developer.android.com/training/location/request-updates?hl=ja
https://developer.android.com/about/versions/14/changes/fgs-types-required?hl=ja

以上