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 で再起動されるとのこと。 ただし、分単位の再起動時間になるようだ。 滅多に再起動が起きないならば数分でも問題はない。