Androidで位置情報取得のベストプラクティス

既に様々な blog で Android での位置情報取得については散々記載されておりますが、各実機を片手に検証していてどうにも腑に落ちなかったので、「やっぱこうだよね」と言える僕的ベストプラクティスをコード例をまじえて記載してみます。尚、必要なポイントのみ記載しているので他の部分についてはよしなに読みかえてくださいね。

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
	<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  ...
</manifest>

Activity クラス

public final class Main extends Activity {

	private LocationManager locationManager;
	private LocationListener locationListener;
	private Timer locationTimer;
	long time;

	void startLocationService() {
		stopLocationService();

		locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
		// 位置情報機能非搭載端末の場合
		if (locationManager == null) {
			// 何も行いません
			return;
		}

		// @see http://developer.android.com/reference/android/location/LocationManager.html#getBestProvider%28android.location.Criteria,%20boolean%29
		final Criteria criteria = new Criteria();
		// PowerRequirement は設定しないのがベストプラクティス
		// Accuracy は設定しないのがベストプラクティス
		//criteria.setAccuracy(Criteria.ACCURACY_FINE);	← Accuracy で最もやってはいけないパターン
		// 以下は必要により
		criteria.setBearingRequired(false);	// 方位不要
		criteria.setSpeedRequired(false);	// 速度不要
		criteria.setAltitudeRequired(false);	// 高度不要

		final String provider = locationManager.getBestProvider(criteria, true);
		if (provider == null) {
			// 位置情報が有効になっていない場合は、Google Maps アプリライクな [現在地機能を改善] ダイアログを起動します。
			new AlertDialog.Builder(this)
				.setTitle("現在地機能を改善")
				.setMessage("現在、位置情報は一部有効ではないものがあります。次のように設定すると、もっともすばやく正確に現在地を検出できるようになります:\n\n● 位置情報の設定でGPSとワイヤレスネットワークをオンにする\n\n● Wi-Fiをオンにする")
				.setPositiveButton("設定", new DialogInterface.OnClickListener() {
					@Override
					public void onClick(final DialogInterface dialog, final int which) {
						// 端末の位置情報設定画面へ遷移
						try {
							startActivity(new Intent("android.settings.LOCATION_SOURCE_SETTINGS"));
						} catch (final ActivityNotFoundException e) {
							// 位置情報設定画面がない糞端末の場合は、仕方ないので何もしない
						}
					}
				})
				.setNegativeButton("スキップ", new DialogInterface.OnClickListener() {
					@Override public void onClick(final DialogInterface dialog, final int which) {}	// 何も行わない
				})
				.create()
				.show();

			stopLocationService();
			return;
		}

		// 最後に取得できた位置情報が5分以内のものであれば有効とします。
		final Location lastKnownLocation = locationManager.getLastKnownLocation(provider);
		// XXX - 必要により判断の基準を変更してください。
		if (lastKnownLocation != null && (new Date().getTime() - lastKnownLocation.getTime()) <= (5 * 60 * 1000L)) {
			setLocation(lastKnownLocation);
			return;
		}

		// Toast の表示と LocationListener の生存時間を決定するタイマーを起動します。
		locationTimer = new Timer(true);
		time = 0L;
		final Handler handler = new Handler();
		locationTimer.scheduleAtFixedRate(new TimerTask() {
			@Override
			public void run() {
				handler.post(new Runnable() {
					@Override
					public void run() {
						if (time == 1000L) {
							Toast.makeText(Main.this, "現在地を特定しています。", Toast.LENGTH_LONG).show();
						} else if (time >= (30 * 1000L)) {
							Toast.makeText(Main.this, "現在地を特定できませんでした。", Toast.LENGTH_LONG).show();
							stopLocationService();
						}
						time = time + 1000L;
					}
				});
			}
		}, 0L, 1000L);

		// 位置情報の取得を開始します。
		locationListener = new LocationListener() {
			@Override
			public void onLocationChanged(final Location location) {
				setLocation(location);
			}
			@Override public void onProviderDisabled(final String provider) {}
			@Override public void onProviderEnabled(final String provider) {}
			@Override public void onStatusChanged(final String provider, final int status, final Bundle extras) {}
		};
		locationManager.requestLocationUpdates(provider, 60000, 0, locationListener);
	}

	void stopLocationService() {
		if (locationTimer != null) {
			locationTimer.cancel();
			locationTimer.purge();
			locationTimer = null;
		}
		if (locationManager != null) {
			if (locationListener != null) {
				locationManager.removeUpdates(locationListener);
				locationListener = null;
			}
			locationManager = null;
		}
	}

	void setLocation(final Location location) {
		stopLocationService();

		// TODO: ここに位置情報が取得できた場合の処理を記述します。
	}

}

ACCURACY_FINE で高精度位置情報のみにしてしまうと高精度位置情報プロバイダの挙動に依存してしまうので、屋内やビル密集地の場合、位置ずれがもっともひどくなります。なので ACCURACY_FINE 指定せずにまかせるのが良いと思われます。

ポイント

  • AndroidManifest.xmlACCESS_FINE_LOCATION のみではなく ACCESS_COARSE_LOCATION も指定
  • Criteria.setAccuracy で Criteria.ACCURACY_FINE を使用しない
  • 適切に設定された Criteria を使用して LocationManager.getBestProvider を行う

他の blog で ACCURACY_FINE をマスト使用するとか、getBestProvider 使っちゃ駄目などと、僕的プラクティスとは真逆の記載が結構あってモンモンとしていたのでちょっと書いて見ました。
この方々は位置情報が帰ってくる/こないに執着してしまっていて情報の正確性まで視点が行っていないように見受けられました。(そもそもは高精度を使用することで情報の正確性を担保したかったのでしょうが、位置情報取得環境により低精度の方が正確な情報が帰ってくるので...)

追記

ちなみに ACCURACY_FINE にていわゆる GPS からの位置情報取得に頼る場合は GpsStatus で接続している衛星の数を確認するなどして精度を確かめた方が良いのではないかと思います。あと、LocationManager.getLastKnownLocation で帰ってくる位置情報は Last known なわけで、情報の鮮度を表す時間を見るのは常識ではないかと思います。

追記2

そういえば GPS プロバイダを使用すると待てど暮らせど LocationListener#onLocationChanged が呼ばれないと記載のあった blog を見ましたが、一応数時間待てば呼ばれるケースもあるみたい。しかし、わざわざ GPS を明示的に使用する目的にはまったくマッチしてないですね。なので高精度プロバイダである GPS プロバイダについては「僕的主観」ですが、レスポンスタイムは数分〜数時間・ときどき早い。また、接続している衛生の数が複数(例えば3つ以上とか)でないと割と正確でない。という印象があります。
一定の時間 (例えばコード例のように30秒間) 内に、これらの条件での GPS プロバイダの位置情報を採用し、駄目だったらベストプロバイダの位置情報を採用するのが理想でしょうか。。。既にクリスマス休暇中なので、早ければ来週にでも検証してみようかと。また blog でお知らせできると良いですね。