トップ地図アプリMap3 > 最初のプログラム

地図アプリMap3: 最初のプログラム

はじめに

プログラム規模が大きくなるほど、特に、プログラム開発のスタートが重要である。後になるほど修正が難しくなる。 急ぐ話ではないので、月日をかけてじっくりと進めよう。

現在使用中の地図アプリMapXではアプリ自体に位置情報取得サービスを含んでいるが、Map3 では別アプリとする。 Map3では直近の位置情報を定期的に取得する[1]。

現在 MapXに含まれる位置情報取得サービスは独立したGPS3アプリとする。これまで通り、GPS軌跡ログをファイルに書き込む。

最初のMap3プログラム

その日のGPS軌跡を地図に表示するために、1日分のログをメモリに保持する。 Map3 を停止させた場合、GPSアプリが書き込んだファイルを読み込むことになるが、 この場合、直近の数分間のデータはまだファイルに書き込まれていないことがある。 致命的な不都合ではないが、なるべく避けたい。GPSアプリに1日分のログを保持して置き、 GPSアプリから受け取る方法も考えられるが、それなりに面倒なプログラムが必要となる。

したがって、Map3アプリも日中は止まらないアプリにした方がプログラムはシンプルになる。 ただし、バッテリ消費が無視できないほど大きい場合は再考を要する。

位置情報を取り続けるには、サービスの場合はフォアグラウンドサービスにする必要があるが、 サービスではなくプロセス(Activity)でも停止しないようにできるようである[2]。 例えば、スギサポwalk(万歩計アプリ)は24時間稼働であるが、 実行中サービスを見ると 「1個のプロセスと0個のサービス」となっている。

しかし、プロセス=Activity、サービス=Service でないのかも知れない。 まずは、Service を使わず、Activityだけのアプリを動かし続けられるか調べてみよう。

記事[3]を参考にして、アプリ起動からの経過時間を表示するプログラム(下記)を作成した。

実機テストはタブレットで行った。記事[2]に従い、設定でアプリmap3について「バッテリの最適化をしない」に変えた。

これで、画面が消えても、プログラムが動き続けていることが確認できた。

しかし、バッテリの消費が大きくなるので、更に一工夫がいるであろう。

最初のプログラム

public class MainActivity extends AppCompatActivity {
    LocalTime time;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tvTime = findViewById(R.id.txtTime);
        final Handler handler = new Handler();

        time = LocalTime.of(0, 0);

        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                //時間に 1sec 加算
                time = time.plusSeconds(1);
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        //画面に現在の経過時間を表示
                        String fmt = time.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
                        tvTime.setText(fmt);
                    }
                });
            }
        }, 0, 1000);    // 1秒間隔
    }
}

直近の位置座標の取得を続ける

package com.example.map3;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.widget.TextView;
import android.widget.Toast;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationServices;

public class MainActivity extends AppCompatActivity {

    private FusedLocationProviderClient fusedLocationClient;

    private LocalTime time;
    TextView tvLocation;

    private final ActivityResultLauncher<String>
            requestPermissionLauncher = registerForActivityResult(
            new ActivityResultContracts.RequestPermission(),
            isGranted -> {
                if (isGranted) {
                    fusedLocation();
                } else {
                    Toast toast = Toast.makeText(this,
                            "これ以上なにもできません", Toast.LENGTH_SHORT);
                    toast.show();
                }
            });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tvLocation = findViewById(R.id.txtTime);

        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);

        if (ActivityCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            requestPermissionLauncher.launch(
                    Manifest.permission.ACCESS_FINE_LOCATION);
        }

        final Handler handler = new Handler();

        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                handler.post(new Runnable() {
                    @Override
                    public void run() { fusedLocation(); }
                });
            }
        }, 0, 1000);    // 1秒間隔
    }

    private void fusedLocation() {
        // 最後に確認された位置情報を取得
        FusedLocationProviderClient fusedLocationClient =
                LocationServices.getFusedLocationProviderClient(this);

        if (ActivityCompat.checkSelfPermission(
                this, Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            return;
        }

        fusedLocationClient.getLastLocation()
                .addOnSuccessListener(this, location -> {
                    if (location != null) {
                        tvLocation.setText(toString(location));
                    }
                });
    }

    String toString(Location loc) {
        long utcMillis = loc.getTime();
        Instant instant = Instant.ofEpochSecond((utcMillis+500)/1000);
        LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        String format = "%02d:%02d:%02d,%.7f,%.7f,%.1f,%.2f,%.1f\n";
        return String.format(Locale.JAPAN, format, ldt.getHour(), ldt.getMinute(), ldt.getSecond(),
                loc.getLongitude(), loc.getLatitude(), loc.getAltitude(),
                loc.getSpeed(), loc.getAccuracy());
    }

}

おわりに

長時間動かし続けるアプリはできるだけ負荷の軽いものにしなければならない。

それ以上に重要なことは精度高く位置情報をとり続けることである。最近インストールした地図アプリを一晩消し忘れていた。 このため、通常より1時間当たり2%ほど電池使用量が増えたようである。

自作地図アプリでは電池使用量を減らすために、すでに相当な工夫をしているが、外出時の電池使用量で大きな比重を占めている。

位置情報取得を別アプリとするか否かは重要ではない。分けて開発して、そのまま運用してもよいし、その後一体化してもよい。 最初から一体化していてもよい。

要するに、これまで通り、フォアグラウンドサービスで位置情報取得を続け、broadcast するとともに、 位置情報ログは1~数分間隔でファイルに追記する。

位置情報取得サービスは常時動いているが、地図アプリは停止してもよい。 再起動したとき、その日の位置情報ログファイルを読み込み、その後は broadcast される位置情報を受け取る。

再起動直後では、ログファイルに書き込まれず、位置情報取得サービスのメモリ上にあるものは欠ける。 1から数分後に再読み込みを行えば、欠けをなくすることができるが、そこまでする必要はないかも知れない。

地図アプリを一日中動かしても電池使用量が少ないようならば、そうした方はプログラムはより簡単になる。 その日のログファイルを読み込む必要はなく、broadcast された位置情報をメモリに蓄積するだけでよい。

リファレンス

[1] 直近の位置情報を取得する
[2] Androidスマホでバックグラウンドアプリが停止するのを防ぐ。HUAWEI Mate 20 Pro+GPSロガー「Geo Tracker」「A-GPS Tracker」の例で。
[3] Javaのタイマー(Timer)を使ってストップウォッチを作ってみよう
[4] [Android] FusedLocationProviderClient を使って位置情報を取得