GPSのログを取り続けるためにはどうするといいのでしょうか。システムはすぐにスリープ、Dozeに入れてバッテリー消費を抑制しようとします。ただし、バックグラウンドなのですがいわゆるforegroundという形での記録ができます。
Android 10
GPSログ
ということで、これ以上はあまり注力するのを止めることとしました
GPSでログを取るためには現実的に裏にいないとできません。ネットサーチをしながらラインをやりながら裏でログを取るということです。ただ昨今のバックグラウンド制限が厳しくなってきたため、それを回避するためにはServiceとNotificationを使ってフォアグランドにいるようにすることで可能です。
この内容とGPS測位、外部ストレージでの保存を使っていきます。
startForegroundService
ActivityからstartForegroundService()でServiceを開始し、Service内で5秒以内に
startForegroundを呼び出します。これでステータスバーにはアイコンが表示されてForgroundにいるかのようになります。
Activity
1 2 3 4 5 6 |
... Intent intent = new Intent(getApplication(), LocationService.class); // API 26 以降 startForegroundService(intent); ... |
Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
... 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 設定 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); } ... |
ここでアイコンは意外と重要で、これが無いと設定できません。理由はおそらく、ユーザーに対してアプリが起動中であることを明示するものだと思います。こっそりバックグラウンドで動き続けるゾンビアプリさせないような施策でしょうか。
サンプルコード
基本的には、GPS パーミッションを考慮して実装する で説明したようにGPSの設定をします。最初にPermissionが必要なので、ACCESS_FINE_LOCATIONとWRITE_EXTERNAL_STORAGEのpermissionを設定。またFOREGROUND_SERVICEのパーミッション設定が必要です。
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" ... <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- API 26 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application ... </activity> <service android:name=".LocationService" /> </application> </manifest> |
MainActivityではパーミッションの確認をします。
許可を確認できるとLocationServiceへ遷移します。今回はログの保存先を外部ストレージにしました。Runtime Permissionの確認が2つになります。
MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
package your.package.name; import androidx.appcompat.app.AppCompatActivity; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; 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.Gravity; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; 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(); } else{ 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 reqPermissions = new ArrayList<>(); // 位置情報の Permission が許可されているか確認 if (permissionLocation == PackageManager.PERMISSION_GRANTED) { // 許可済 } else{ // 未許可 reqPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION); } // 外部ストレージ書き込みが許可されているか確認 if (permissionExtStorage == PackageManager.PERMISSION_GRANTED) { // 許可済 } else{ // 許可をリクエスト reqPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); } // 未許可 if (!reqPermissions.isEmpty()) { ActivityCompat.requestPermissions(this, reqPermissions.toArray(new String[0]), REQUEST_MULTI_PERMISSIONS); // 未許可あり } else{ // 許可済 startLocationService(); } } // 結果の受け取り @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) { // 許可された } else { // それでも拒否された時の対応 toastMake("位置情報の許可がないので計測できません"); } } // 外部ストレージ else if (permissions[i]. equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { // 許可された } else { // それでも拒否された時の対応 toastMake("外部書込の許可がないので書き込みできません"); } } } startLocationService(); } } else{ // } } 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 以降 startForegroundService(intent); // Activityを終了させる finish(); } }); 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.setGravity(Gravity.CENTER, 0, 200); toast.show(); } } |
ここから位置測定のコードになります。
「Start」ボタンで測位と書き込みを実行することができ位置情報を取得すると外部ストレージに保存します。
「Log」ボタンで保存した履歴を表示させます。
「Reset」で履歴を消し、GPSも止めます。
LocationService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
package your.package.name; import androidx.core.app.ActivityCompat; 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.icu.text.SimpleDateFormat; import android.icu.util.TimeZone; 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 android.util.Log; public class LocationService extends Service implements LocationListener{ private LocationManager locationManager; private Context context; private static final int MinTime = 1000; private static final float MinDistance = 50; 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 設定 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 = " +String.valueOf(location.getLatitude()) + "\n"; strBuf.append(str); str = "Longitude = " + String.valueOf(location.getLongitude()) + "\n"; strBuf.append(str); str = "Accuracy = " + String.valueOf(location.getAccuracy()) + "\n"; strBuf.append(str); str = "Altitude = " + String.valueOf(location.getAltitude()) + "\n"; strBuf.append(str); SimpleDateFormat sdf = new SimpleDateFormat("MM/dd HH:mm:ss"); sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo")); String currentTime = sdf.format(location.getTime()); str = "Time = " + currentTime + "\n"; strBuf.append(str); str = "Speed = " + String.valueOf(location.getSpeed()) + "\n"; strBuf.append(str); str = "Bearing = " + String.valueOf(location.getBearing()) + "\n"; strBuf.append(str); strBuf.append("----------\n"); 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) { // Android 6, API 23以上でパーミッシンの確認 if(Build.VERSION.SDK_INT <= 28){ StringBuilder strBuf = new StringBuilder(); 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; } } |
次に、外部ストレージへの書き込み、読み出しのクラスを作ります。
StorageReadWrite.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
package your.package.name; 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 */ boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); return (Environment.MEDIA_MOUNTED.equals(state)); } /* Checks if external storage is available to at least read */ boolean isExternalStorageReadable() { String state = Environment.getExternalStorageState(); return (Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)); } } |
レイアウトはこのようになります。呼び出した履歴はスクロールで表示させます。
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<?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"> <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:textSize="18sp" android:layout_margin="5dp" android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content" /> <Button android:id="@+id/button_log" android:text="@string/log" android:textSize="18sp" android:layout_margin="5dp" android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content" /> <Button android:id="@+id/button_reset" android:text="@string/reset" android:textSize="18sp" android:layout_margin="5dp" 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> |
リソースです
strings.xml
1 2 3 4 5 6 |
<resources> <string name="app_name">YourAppName</string> <string name="start">Start</string> <string name="log">Log</string> <string name="reset">Reset</string> </resources> |
このサンプルコードでできたアプリは、マルチタスク ウィンドウから消しても動き続けます。明確にストップさせない限り動き続けますので、実際バッテリーの減りが早くなりました。
関連ページ:
- FusedLocationProviderClient による位置情報取得
- GPSでの位置情報
- GPS パーミッションを考慮して実装する
- GPSログをテキストで保存、複数の Runtime Permissionの設定
- バックグラウンドでGPSログを取り続けるには
- Runtime Permission, Android 6.0 からの変更
References:
LocationManager | Android Developers
Criteria | Android Developers
Runtime Permissions | Android Developers
Location Strategies | Android Developers
Location | Android Developers