トップAndroid Java > GPSログを取り続ける方法

GPSログを取り続ける方法

1.はじめに

Androidスマホのプログラム作成も1年近くなり、だんだん分かってきた。

プログラムは、しばらく GIS(地図アプリ本体)、OSM(レンダリング)、GPS(位置情報取得)に分けていたが、 プログラムが分かってきたので、まず、OSM を GIS に吸収統合した。

地図アプリのレンダリングパフォーマンスは当初に比べて桁違いに向上したが、 そろそろ限界に近づいてきたので、次に、GPS を GIS に吸収統合した。

ただし、統合地図アプリGISの起動停止を繰り返すと、Service で同じ位置情報が複数取得される。 また、取得した位置情報は broadcast しているが、Activity は一つの broadcast を複数受け取っている。

あたかも、サービスまたは位置情報取得リスナーが複数起動され、更に、broadcast のレシーバーが複数登録されているような振る舞いとなる。

応急処理として、同じ位置情報は無視するようにしているが、内部的に無駄が発生しているため、 上記の二つの不具合は究明する必要がある。

アプリやサービスの起動、停止処理のどこかに誤まり、漏れなどがあるのであろう。 位置情報取得は、当初は、GIS に一体化していたため、当初の記録も辿ってみたい。

2.プログラム修正記録

次章のこれまでの記録を参考にして、プログラムを修正する。


[2023.4.23]

LocalBroadCastManager は現在はサポートされていない。 「代わりに、LiveData ストリームまたはリアクティブ ストリームを使用してください。」 とある。 Localbroadcastmanager

1.位置情報は文字列化せず、以前のように Loaction のインスタンスを送る[とりやめ]

赤字の部分は必須かどうか不明である。取りあえずは、なしとする。

    // 現在のプログラム
    Intent broadcast = new Intent();
    String strLocation = Utils.toString(location);
    broadcast.putExtra(EXTRA_LOCATION, strLocation);
    broadcast.setAction(ACTION_BROADCAST);
    getBaseContext().sendBroadcast(broadcast);

    class MyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Bundle extras = intent.getExtras();
            String location = extras.getString("com.example.gis.location");
            gis.updateLocation(location);       // 現在位置追跡
            GPSLog log = new GPSLog(location);  // 新たなログポイント
            GPSLog.listLogs.add(log);
        }
    }


    // 以前のプログラム
    Intent intent = new Intent(ACTION_BROADCAST);
    intent.putExtra(EXTRA_LOCATION, location);
    LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);

    // Update notification content if running as a foreground service.
    if (serviceIsRunningInForeground(this)) {
        mNotificationManager.notify(NOTIFICATION_ID, getNotification());
    }

    class MyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Location location = intent.getParcelableExtra(LocationUpdatesService.EXTRA_LOCATION);
            if (location != null) {
                GPSLog log = new GPSLog(location);  // 新たなログポイント
                GPSLog.listLogs.add(log);
            }
        }
    }

この段階では、位置情報は取得されるが、例えば3回起動すると、同じデータが3つ続く。 BroadcastReceiver がキャッチしない。 もし、赤字部分が必要ならば、前のプログラムの方が分かりやすい。

LocalBroadcastManagerの問題かも知れないが、元のプログラムに戻した。

2.再起動で同じ位置情報を取得する原因を調べる

onStart、onPouse、onStop などでの処理が漏れているのであろう。

GISを(再)起動したとき、LocationUpdatesServiceが起動中かどうか判定して、 起動中でない場合に限り起動するように変更した。これで位置情報取得のダブりはなくなった。

3.再起動で broadcast の受信が重複する原因を調べる

GISアプリを finish しても、プロセスは残っており、登録された broadcast のレシーバーは有効なのであろう。 再起動ににより、あらたに、broadcast のレシーバーが登録され、レシーバーが複数動作しているものと思われる。

GISを停止するときに、broadcastレシーバーの登録を解除するか、または、 再起動にbroadcastレシーバーの登録の有無をチェックして、多重登録を避ける必要がある。

onCreate でレシーバーを登録しているので、onDestroy で削除すべきであろう。

3.プログラム作成履歴

自アプリ内のサービスから位置情報を受ける

当初のGISアプリでは次のようにして自アプリ内の service (LocationUpdatesService)からの 位置情報の broadcast を受けていた。

public class GPSLogger {
    final private GIS gis;

    // The BroadcastReceiver used to listen for broadcasts from the service.
    public MyReceiver myReceiver;

    public GPSLogger(GIS gis) {
        this.gis = gis;
        myReceiver = new MyReceiver();
    }

    // Receiver for broadcasts sent by {@link LocationUpdatesService}.
    private class MyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Location location = 
                  intent.getParcelableExtra(LocationUpdatesService.EXTRA_LOCATION);
            if (location != null) {
                gis.updateLocation(location);       // 現在位置追跡
                GPSLog log = new GPSLog(location);  // 新たなログポイント
                GPSLog.listLogs.add(log);
                //Log.d("receiver", "update");
            }
        }
    }

}

送り側のプログラムを下に示す。EXTRA_LOCATION は 識別子に過ぎない。 実際のプログラムでは "com.example.gis.location" である。 これはどんな文字列でもいいはず。

問題は送る側と受ける側のアプリが異なる場合、 Location の定義が同じであれば、OKかどうかである。もし、ダメな場合は String にすればよい。

    private void onNewLocation(Location location) {
        Log.i(TAG, "New location: " + location);

        mLocation = location;

        // Notify anyone listening for broadcasts about the new location.
        Intent intent = new Intent(ACTION_BROADCAST);
        intent.putExtra(EXTRA_LOCATION, location);
        LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);

        // Update notification content if running as a foreground service.
        if (serviceIsRunningInForeground(this)) {
            mNotificationManager.notify(NOTIFICATION_ID, getNotification());
        }
    }

GISアプリ内の service からの broadcast を止めて、 GPSアプリ内の service からの broacast を受けるか試した。 GPS側のEXTRA_LOCATIONは GIS側に合わせて "com.example.gis.location" とした。

ACTION_BROADCASTも GIS側に合わせた。

受信しない!! 多分、LocalBroadcastManagerから送るものは他のアプリには届かないのであろう。

別アプリから位置情報を受ける

以前に作成したGPSアプリ(パッケージ名 LUFS)は公式事例[14]そのものか、あるいはほんの少し 修正しただけのものであろう。

このアプリから位置情報を broacast して、別のアプリでこれを受信してみる。

現在は自アプリ向けの broadcast のため、これに他アプリ向けの broadcast を赤字で追加した。 自アプリ向けには Location のようにオブジェクトが送れることは確かであるが、 他アプリ向けに可能かどうかは未調査である。

まずは無難に位置情報を文字列として送る。

    protected void sendMessage(String msg){
        Intent broadcast = new Intent();
        broadcast.putExtra("message", msg);
        broadcast.setAction("DO_ACTION");
        getBaseContext().sendBroadcast(broadcast);
    }

    private void onNewLocation(Location location) {
        Log.i(TAG, "New location: " + location);

        String msg = Utils.getLocationText(location);
        sendMessage(msg);

        mLocation = location;

        // Notify anyone listening for broadcasts about the new location.
        Intent intent = new Intent(ACTION_BROADCAST);
        intent.putExtra(EXTRA_LOCATION, location);
        LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);

        //getBaseContext().sendBroadcast(intent);

        // Update notification content if running as a foreground service.
        if (serviceIsRunningInForeground(this)) {
            mNotificationManager.notify(NOTIFICATION_ID, getNotification());
        }
    }

この例では受ける側のプログラムは以下のようになる。 "DO_ACTION" は送り側と受け側の合言葉であり、別の文字列でもよい。

これで位置情報が正常に送受信できることを確認した。

String ではなく Location オブジェクト(インスタンス)も送れるようだが[17]、 少なくとも当面は文字列で送受信した方が無難であろう。

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

        UpdateReceiver receiver = new UpdateReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction("DO_ACTION");
        registerReceiver(receiver, filter);
    }

    class UpdateReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent){
            Bundle extras = intent.getExtras();
            String msg = extras.getString("message");
            Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
        }
    }

manifestで宣言した場合、赤字の4行は要らないようだ。。

位置情報取得サービス

通常のサービスは動き続ける。しかし、以前の経験では、 位置情報取得サービスの場合、バッテリー消費を減らすためか、しばらく動いていたが、止まってしまった。 定期的にログをファイルに書き込んでいても、止まってしまうかどうか再確認したい。

バッググラウンドが前提であり、フォアグラウンドでは位置情報取得が止まってもよい。

位置情報取得およびファイルアクセス権限はスマホで許可した。

Manifest

以下を追加して、スマホで権限を許可した。

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

常にバッググラウンドサービスとなるプログラムを下に示す。 やはり、電池消費量を抑えるために、途中で止まってしまう可能性は高い。 ログをファイルに書き込む機能を追加して、外出でテストしないと分からない。

public class LocationUpdatesService extends Service {
    private static final String TAG = LocationUpdatesService.class.getSimpleName();

    static final String EXTRA_LOCATION = "com.example.gps.location";

    private static final long UPDATE_INTERVAL_IN_MILLISECONDS = 5000;
    private static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = 2000;
    private static final long MAX_WAITTIME_IN_MILLISECONDS = 20000;

    private LocationRequest mLocationRequest;
    private FusedLocationProviderClient mFusedLocationClient;
    private LocationCallback mLocationCallback;

    private int cntSkip = 0;

    public LocationUpdatesService() {
        Log.d(TAG, "LocationUpdatesService");
    }

    @Override
    public void onCreate() {
        mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);

        mLocationCallback = new LocationCallback() {
            @Override
            public void onLocationResult(@NonNull LocationResult locationResult) {
                super.onLocationResult(locationResult);
                onNewLocation(locationResult.getLastLocation());
            }

            private void onNewLocation(Location location) {
                Log.d(TAG, getLocationText(location));
                if (location.getSpeed() <= 0.05 && cntSkip < 20) {
                    if (++cntSkip % 10 == 0) {
                        Log.i(TAG, "skip " + cntSkip + " speed=" + location.getSpeed());
                    }
                    return;
                }
                cntSkip = 0;
            }
        };

        // Sets the location request parameters.
        createLocationRequest();

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED &&
                ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
                        != PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "権限がありません", Toast.LENGTH_LONG).show();
            return;
        }
        mFusedLocationClient.requestLocationUpdates(mLocationRequest,
                mLocationCallback, Looper.getMainLooper());
    }

    static String getLocationText(Location location) {
        return location == null ? "Unknown location" :
                "(" + location.getLatitude() + ", " + location.getLongitude() + ")";
    }

    // Sets the location request parameters.
    private void createLocationRequest() {
        mLocationRequest = new LocationRequest();
        mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);
        mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);
        mLocationRequest.setMaxWaitTime(MAX_WAITTIME_IN_MILLISECONDS);
        mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand Received start id " + startId + ": " + intent);
        Toast.makeText(this, "Service#onStartCommand", Toast.LENGTH_SHORT).show();
        // 強制終了時、システムによる再起動を求める場合はSTART_STICKYを利用
        // 再起動が不要な場合はSTART_NOT_STICKYを利用する
        return START_STICKY;
    }


    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}


public class MainActivity extends AppCompatActivity {
    private MyReceiver myReceiver;
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.textview);

        myReceiver = new MyReceiver();
        textView.setText("startService");
        startService(new Intent(MainActivity.this, LocationUpdatesService.class));
    }

    // Receiver for broadcasts sent by {@link LocationUpdatesService}.
    private class MyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Location location = intent.getParcelableExtra(LocationUpdatesService.EXTRA_LOCATION);
            if (location != null) {
                GPSLog log = new GPSLog(location);  // 新たなログポイント
                GPSLog.listLogs.add(log);
            }
        }
    }

}

START_STICKYの場合、再起動は迅速であるが、intent が null になる可能性があるらしい。 これを START_REDELIVER_INTENT に変えれば、起動時の intent で再起動されるとのこと。 ただし、分単位の再起動時間になるようだ。 滅多に再起動が起きないならば数分でも問題はない。

A.リファレンス

[1] 電池の最適化をオフにする方法
[2] デバイスの起動状態を維持する
[3] [Android] バックグラウンドでGPSログを取り続けるには
[4] 常駐するサービスの作成
[5] AndroidアプリでForeground Serviceを使って、画面スリープ状態でも位置情報を定期取得する
[6] [Android] Service の使い方
[7] ウェイクロック: Android* アプリケーションでスリープしない問題の検出
[8] 直近の位置情報を取得する
[9] open-gpstracker
[10] ブロードキャストレシーバの実装によるアクティビティとサービスの通信
[11] バックグラウンドでの位置情報へのアクセス
[12] 現在地の更新情報をリクエストする
[13] android / location-samples
[14] LocationUpdatesForegroundService
[15] location-samples/LocationUpdatesPendingIntent/
[16] AndroidアプリでForeground Serviceを使って、画面スリープ状態でも位置情報を定期取得する[kt]
[17] Androidでよく書く基本的な記述集(その1)
[18] SERVICEを使う(1) LOCALSERVICEによる常駐型アプリ