Androidスマホのプログラム作成も1年近くなり、だんだん分かってきた。
プログラムは、しばらく GIS(地図アプリ本体)、OSM(レンダリング)、GPS(位置情報取得)に分けていたが、 プログラムが分かってきたので、まず、OSM を GIS に吸収統合した。
地図アプリのレンダリングパフォーマンスは当初に比べて桁違いに向上したが、 そろそろ限界に近づいてきたので、次に、GPS を GIS に吸収統合した。
ただし、統合地図アプリGISの起動停止を繰り返すと、Service で同じ位置情報が複数取得される。 また、取得した位置情報は broadcast しているが、Activity は一つの broadcast を複数受け取っている。
あたかも、サービスまたは位置情報取得リスナーが複数起動され、更に、broadcast のレシーバーが複数登録されているような振る舞いとなる。
応急処理として、同じ位置情報は無視するようにしているが、内部的に無駄が発生しているため、 上記の二つの不具合は究明する必要がある。
アプリやサービスの起動、停止処理のどこかに誤まり、漏れなどがあるのであろう。 位置情報取得は、当初は、GIS に一体化していたため、当初の記録も辿ってみたい。
次章のこれまでの記録を参考にして、プログラムを修正する。
LocalBroadCastManager は現在はサポートされていない。 「代わりに、LiveData ストリームまたはリアクティブ ストリームを使用してください。」 とある。 Localbroadcastmanager
赤字の部分は必須かどうか不明である。取りあえずは、なしとする。
// 現在のプログラム 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の問題かも知れないが、元のプログラムに戻した。
onStart、onPouse、onStop などでの処理が漏れているのであろう。
GISを(再)起動したとき、LocationUpdatesServiceが起動中かどうか判定して、 起動中でない場合に限り起動するように変更した。これで位置情報取得のダブりはなくなった。
GISアプリを finish しても、プロセスは残っており、登録された broadcast のレシーバーは有効なのであろう。 再起動ににより、あらたに、broadcast のレシーバーが登録され、レシーバーが複数動作しているものと思われる。
GISを停止するときに、broadcastレシーバーの登録を解除するか、または、 再起動にbroadcastレシーバーの登録の有無をチェックして、多重登録を避ける必要がある。
onCreate でレシーバーを登録しているので、onDestroy で削除すべきであろう。
当初の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行は要らないようだ。。
通常のサービスは動き続ける。しかし、以前の経験では、 位置情報取得サービスの場合、バッテリー消費を減らすためか、しばらく動いていたが、止まってしまった。 定期的にログをファイルに書き込んでいても、止まってしまうかどうか再確認したい。
バッググラウンドが前提であり、フォアグラウンドでは位置情報取得が止まってもよい。
位置情報取得およびファイルアクセス権限はスマホで許可した。
以下を追加して、スマホで権限を許可した。
<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 で再起動されるとのこと。 ただし、分単位の再起動時間になるようだ。 滅多に再起動が起きないならば数分でも問題はない。