Global lo0

One good turn deserves another. GPS×ImageProcess×...

AndroidでGPSの取得アプリを作った話

AndroidGPSの取得をバックグラウンドで行います

f:id:kuri_megane:20190901230015j:plain


ざっくりまとめると...

  • 測位したGPSをテキストファイルに書き出すアプリを作りました
  • バッググラウンドで継続実行することができます


各種環境

どんなアプリ?

こちらの記事で紹介したラズパイで測位したGPSとの比較を行えるようできる限り設定を合わせたものです.

kuri-megane.hatenablog.jp

アプリとしてはGPSを測位してファイルに書き出すものです.

あまりうまく動かない場合もあり,備忘録として書き残します.

動くとこんな感じ

(画像がでかくてすみません)

f:id:kuri_megane:20190901225013p:plain
起動画面

f:id:kuri_megane:20190901225021p:plain
動作してる様子

ソースコード

ほとんどこちらの記事と同じです.

うまく動かなかったところなどを少しアレンジしました.

akira-watson.com

Main.java

package io.github.kuri_megane.evaluate_gps_android;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import java.util.ArrayList;


public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_MULTI_PERMISSIONS = 101;

    private TextView textView;
    private StorageReadWrite fileReadWrite;

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

        Context context = getApplicationContext();
        fileReadWrite = new StorageReadWrite(context);

        // Android 6, API 23以上でパーミッシンの確認
        if (Build.VERSION.SDK_INT >= 23) {
            checkMultiPermissions();
        }

        startLocationService();

    }

    // 位置情報許可の確認、外部ストレージのPermissionにも対応できるようにしておく
    private void checkMultiPermissions() {

        // 位置情報の Permission
        int permissionLocation = ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION
        );
        // 外部ストレージ書き込みの Permission
        int permissionExtStorage = ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
        );

        ArrayList<String> reqPermissions = new ArrayList<>();

        // 位置情報の Permission が許可されているか確認
        if (permissionLocation != PackageManager.PERMISSION_GRANTED) {
            reqPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
        }

        // 外部ストレージ書き込みが許可されているか確認
        if (permissionExtStorage != PackageManager.PERMISSION_GRANTED) {
            reqPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
        }

        // 未許可
        if (!reqPermissions.isEmpty()) {
            ActivityCompat.requestPermissions(
                    this,
                    reqPermissions.toArray(new String[0]),
                    REQUEST_MULTI_PERMISSIONS
            );
        }
    }

    // 結果の受け取り
    @Override
    public void onRequestPermissionsResult(
            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults
    ) {

        if (requestCode == REQUEST_MULTI_PERMISSIONS) {
            if (grantResults.length > 0) {
                for (int i = 0; i < permissions.length; i++) {
                    // 位置情報
                    if (permissions[i].
                            equals(Manifest.permission.ACCESS_FINE_LOCATION)) {
                        if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                            toastMake("位置情報の許可がないので計測できません");
                        }
                    }
                    // 外部ストレージ
                    else if (permissions[i].
                            equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                        if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                            toastMake("外部書込の許可がないので書き込みできません");
                        }
                    }
                }
            }
        }
    }

    private void startLocationService() {
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.log_text);

        Button buttonStart = findViewById(R.id.button_start);
        buttonStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getApplication(), LocationService.class);

                // API 26 以降
                if (Build.VERSION.SDK_INT >= 26) {
                    startForegroundService(intent);
                } else {
                    startService(intent);
                }

                // Activityを終了させる
                finish();
            }
        });

        Button buttonStop = findViewById(R.id.button_stop);
        buttonStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Serviceの停止
                Intent intent = new Intent(getApplication(), LocationService.class);
                stopService(intent);
            }
        });

        Button buttonLog = findViewById(R.id.button_log);
        buttonLog.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                textView.setText(fileReadWrite.readFile());
            }
        });

        Button buttonReset = findViewById(R.id.button_reset);
        buttonReset.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Serviceの停止
                Intent intent = new Intent(getApplication(), LocationService.class);
                stopService(intent);

                fileReadWrite.clearFile();
                textView.setText("");
            }
        });
    }

    // トーストの生成
    private void toastMake(String message) {
        Toast toast = Toast.makeText(this, message, Toast.LENGTH_LONG);
        toast.show();
    }
}

測位するバッググラウンドサービスです.

MinTime で 測位時間間隔,MinDistance で最小移動距離(ここで指定した距離を超えるとトリガーが発動)を設定しています.

測位した情報の文字列をフォーマットして StorageReadWrite に渡します.

LocationService.java

package io.github.kuri_megane.evaluate_gps_android;

import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.Settings;

import androidx.core.app.ActivityCompat;

import java.text.SimpleDateFormat;
import java.util.Locale;


public class LocationService extends Service implements LocationListener {

    private LocationManager locationManager;
    private Context context;

    private static final int MinTime = 1000;
    private static final float MinDistance = 0;

    private StorageReadWrite fileReadWrite;

    @Override
    public void onCreate() {
        super.onCreate();

        context = getApplicationContext();
        // 内部ストレージにログを保存
        fileReadWrite = new StorageReadWrite(context);

        // LocationManager インスタンス生成
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
    }

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

        int requestCode = 0;
        String channelId = "default";
        String title = context.getString(R.string.app_name);

        PendingIntent pendingIntent =
                PendingIntent.getActivity(context, requestCode,
                        intent, PendingIntent.FLAG_UPDATE_CURRENT);

        // ForegroundにするためNotificationが必要、Contextを設定
        NotificationManager notificationManager =
                (NotificationManager) context.
                        getSystemService(Context.NOTIFICATION_SERVICE);

        // Notification Channel 設定
        if (Build.VERSION.SDK_INT >= 26) {
            NotificationChannel channel = new NotificationChannel(
                    channelId, title, NotificationManager.IMPORTANCE_DEFAULT);
            channel.setDescription("Silent Notification");
            // 通知音を消さないと毎回通知音が出てしまう
            // この辺りの設定はcleanにしてから変更
            channel.setSound(null, null);
            // 通知ランプを消す
            channel.enableLights(false);
            channel.setLightColor(Color.BLUE);
            // 通知バイブレーション無し
            channel.enableVibration(false);

            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel);
                Notification notification = new Notification.Builder(context, channelId)
                        .setContentTitle(title)
                        // 本来なら衛星のアイコンですがandroid標準アイコンを設定
                        .setSmallIcon(android.R.drawable.btn_star)
                        .setContentText("GPS")
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .setWhen(System.currentTimeMillis())
                        .build();

                // startForeground
                startForeground(1, notification);
            }
        }

        startGPS();

        return START_NOT_STICKY;
    }

    protected void startGPS() {
        StringBuilder strBuf = new StringBuilder();
        strBuf.append("startGPS\n");

        final boolean gpsEnabled
                = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
        if (!gpsEnabled) {
            // GPSを設定するように促す
            enableLocationSettings();
        }

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

                locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
                        MinTime, MinDistance, this);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            strBuf.append("locationManager=null\n");
        }
    }

    @Override
    public void onLocationChanged(Location location) {

        StringBuilder strBuf = new StringBuilder();

        strBuf.append("#----------\n");

        String str = "# Latitude = " + location.getLatitude() + "\n";
        strBuf.append(str);

        str = "# Longitude = " + location.getLongitude() + "\n";
        strBuf.append(str);

        str = "# Accuracy = " + location.getAccuracy() + "\n";
        strBuf.append(str);

        str = "# Altitude = " + location.getAltitude() + "\n";
        strBuf.append(str);

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.JAPAN);
        String currentTime = sdf.format(location.getTime());

        str = "# Time = " + currentTime + "\n";
        strBuf.append(str);

        str = "# Speed = " + location.getSpeed() + "\n";
        strBuf.append(str);

        str = "# Bearing = " + location.getBearing() + "\n";
        strBuf.append(str);

        strBuf.append("# ----------\n");

        str = currentTime + ","
                + location.getLongitude() + ","
                + location.getLatitude() + ","
                + location.getAltitude() + ","
                + location.getAccuracy() + ","
                + location.getSpeed() + ","
                + location.getBearing() + ","
                + "\n";
        strBuf.append(str);

        fileReadWrite.writeFile(strBuf.toString(), true);
    }

    @Override
    public void onProviderDisabled(String provider) {
    }

    @Override
    public void onProviderEnabled(String provider) {
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {

        StringBuilder strBuf = new StringBuilder();

        // Android 6, API 23以上でパーミッシンの確認
        if (Build.VERSION.SDK_INT <= 28) {
            switch (status) {
                case LocationProvider.AVAILABLE:
                    //strBuf.append("LocationProvider.AVAILABLE\n");
                    break;
                case LocationProvider.OUT_OF_SERVICE:
                    strBuf.append("LocationProvider.OUT_OF_SERVICE\n");
                    break;
                case LocationProvider.TEMPORARILY_UNAVAILABLE:
                    strBuf.append("LocationProvider.TEMPORARILY_UNAVAILABLE\n");
                    break;
            }
        }

        fileReadWrite.writeFile(strBuf.toString(), true);
    }

    private void enableLocationSettings() {
        Intent settingsIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
        startActivity(settingsIntent);
    }

    private void stopGPS() {
        if (locationManager != null) {
            // update を止める
            if (ActivityCompat.checkSelfPermission(this,
                    Manifest.permission.ACCESS_FINE_LOCATION) !=
                    PackageManager.PERMISSION_GRANTED &&
                    ActivityCompat.checkSelfPermission(this,
                            Manifest.permission.ACCESS_COARSE_LOCATION) !=
                            PackageManager.PERMISSION_GRANTED) {
                return;
            }
            locationManager.removeUpdates(this);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        stopGPS();
    }

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

StorageWrite.java

package io.github.kuri_megane.evaluate_gps_android;

import android.content.Context;
import android.os.Environment;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;

class StorageReadWrite {

    private File file;
    private StringBuffer stringBuffer;

    StorageReadWrite(Context context) {
        File path = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
        file = new File(path, "log.txt");
    }

    void clearFile() {
        // ファイルをクリア
        writeFile("", false);

        // StringBuffer clear
        stringBuffer.setLength(0);
    }

    // ファイルを保存
    void writeFile(String gpsLog, boolean mode) {

        if (isExternalStorageWritable()) {
            try (FileOutputStream fileOutputStream =
                         new FileOutputStream(file, mode);
                 OutputStreamWriter outputStreamWriter =
                         new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8);
                 BufferedWriter bw =
                         new BufferedWriter(outputStreamWriter)
            ) {

                bw.write(gpsLog);
                bw.flush();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

    // ファイルを読み出し
    String readFile() {
        stringBuffer = new StringBuffer();

        // 現在ストレージが読出しできるかチェック
        if (isExternalStorageReadable()) {

            try (FileInputStream fileInputStream =
                         new FileInputStream(file);

                 InputStreamReader inputStreamReader =
                         new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);

                 BufferedReader reader =
                         new BufferedReader(inputStreamReader)) {

                String lineBuffer;

                while ((lineBuffer = reader.readLine()) != null) {
                    stringBuffer.append(lineBuffer);
                    stringBuffer.append(System.getProperty("line.separator"));
                }

            } catch (Exception e) {
                stringBuffer.append("error: FileInputStream");
                e.printStackTrace();
            }
        }

        return stringBuffer.toString();
    }


    /* Checks if external storage is available for read and write */
    private boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        return (Environment.MEDIA_MOUNTED.equals(state));
    }

    /* Checks if external storage is available to at least read */
    private boolean isExternalStorageReadable() {
        String state = Environment.getExternalStorageState();
        return (Environment.MEDIA_MOUNTED.equals(state) ||
                Environment.MEDIA_MOUNTED_READ_ONLY.equals(state));
    }
}

res/values/strings.xml

<resources>
    <string name="app_name">evaluate-gps-android</string>
    <string name="start">Start</string>
    <string name="stop">Stop</string>
    <string name="log">Log</string>
    <string name="reset">Reset</string>
</resources>

res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#cdf"
    tools:context=".MainActivity">

    <!-- TODO: LinerLayoutはあまり良くないかも... -->

    <LinearLayout
        android:gravity="center"
        android:background="#48f"
        android:orientation="horizontal"
        android:layout_margin="20dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:id="@+id/button_start"
            android:text="@string/start"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content" />

        <Button
            android:id="@+id/button_stop"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/stop" />

        <Button
            android:id="@+id/button_log"
            android:text="@string/log"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content" />

        <Button
            android:id="@+id/button_reset"
            android:text="@string/reset"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <ScrollView
        android:layout_margin="20dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/log_text"
            android:textColor="#000"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </ScrollView>

</LinearLayout>

最後に

この記事で紹介したソースコードGithubで公開しています. --> こちら

この記事の内容で準天頂衛星みちびきと従来GPSの測位結果を比較した記事はこちら!

kuri-megane.hatenablog.jp

参考記事

akira-watson.com