AndroidでGPSの取得アプリを作った話
ざっくりまとめると...
- 測位したGPSをテキストファイルに書き出すアプリを作りました
- バッググラウンドで継続実行することができます
各種環境
- 検証したAndroid: 5.0.2, 8.1, 9.0
- Android Studio: 3.4.2
どんなアプリ?
こちらの記事で紹介したラズパイで測位したGPSとの比較を行えるようできる限り設定を合わせたものです.
アプリとしてはGPSを測位してファイルに書き出すものです.
あまりうまく動かない場合もあり,備忘録として書き残します.
動くとこんな感じ
(画像がでかくてすみません)
ソースコード
ほとんどこちらの記事と同じです.
うまく動かなかったところなどを少しアレンジしました.
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の測位結果を比較した記事はこちら!