A Day In The Life

とあるプログラマの備忘録

Android の位置情報を確実に取る方法

Android の位置情報は端末や OS のバージョンによって微妙にとれるタイミングやプロバイダがちがうので iPhone にくらべかなり癖があります。中には LocationManager#requestLocationUpdates メソッドを呼んでも30分以上メソッドが呼び出されないなんて事もありました。
色々検証した結果、確実に位置情報をとるためのポイントがいくつかありました。以下そのポイントです。

  • 一般的なAndroid端末で取得できる位置情報は GPS、3G、Wi-Fiの3種類
    GPS は「gps」、3G および Wi-Fi は「network」という文字列で取得できます。Android では「gps」や「network」のことをプロバイダと呼びます。
  • Android 2.2 からプロバイダに passive が追加された
    このプロバイダーがどのような用途で使われるのか今のところ不明です。
  • LocationManager#getBestProvider メソッドは不具合があるので使わない
    詳細はこちらの記事を確認してください。
    DesireのGPS取得での試行錯誤
    ※ただ手元の Desire では WakeLock 問題は起こりませんでした。
  • LocationManager#removeUpdates メソッドを呼ばずにアクティビティを終了すると挙動がおかしくなる
    requestLocationUpdates したままバックボタンを押してアクティビティを終了させると次回起動時に onLocationChanged メソッドがしばらく呼び出されなくなりました 。
  • onStatusChanged イベントはあまりあてにならない
    onStatusChanged イベントはプロバイダが使用不能になるまで呼び出されません。使用不能状態でリスナー登録してもこのメソッドは呼び出されませんでした。したがって現在のプロバイダの状態を知りたいときには使えません。
  • Docomoオープンiエリアの位置情報は Android からは取得できない
    Docomo 端末ではガラケーでは位置情報が取得できるのに Android だと取得できないなんてことが起こるようです。

検証に使用したプログラム

以下のサンプルプログラムで検証しました。
プログラムのイメージ

クラスの宣言と初期化処理

LocationManager のインスタンスを取得してプロバイダの情報を表示しています。

public class LocationCheckerActivity extends Activity
    implements LocationListener, GpsStatus.Listener, View.OnClickListener {
  private static final int ID_LOCATION_PROVIDER_ENABLED = 0;
  private static final int ID_LOCATION_PROVIDER_STATUS = 1;
  private static final String PROVIDER_ENABLED = " ENABLED ";
  private static final String PROVIDER_DISABLED = " DISABLED ";	
  private LocationManager locationManager;
  private Map<String, LinearLayout> layoutMap = new HashMap<String, LinearLayout>();

  /* TextViewの宣言省略 */

  @Override
  public void onCreate(Bundle savedInstanceState)
      status.setText("AVAILABLE"); {
    super.onCreate(savedInstanceState);	
    setContentView(R.layout.main);
    
    /* TextViewインスタンスの取得およびリスナーの設定省略 */

    locationManager = (LocationManager)getSystemService(LOCATION_SERVICE);
    locationManager.addGpsStatusListener(this);
    List<String> providers = locationManager.getAllProviders();
    // プロバイダの状態を表示
    LinearLayout layout = (LinearLayout)findViewById(R.id.layout);
    for (String provider : providers) {
      LinearLayout row = new LinearLayout(this);
      layoutMap.put(provider, row);
      TextView label = new TextView(this);
      TextView providerName = new TextView(this);
      TextView enabled = new TextView(this);
      enabled.setId(ID_LOCATION_PROVIDER_ENABLED);
      String e = locationManager.isProviderEnabled(provider)? PROVIDER_ENABLED : PROVIDER_DISABLED;
      enabled.setText(e);
      TextView status = new TextView(this);
      status.setId(ID_LOCATION_PROVIDER_STATUS);
      label.setText("Provider Status:");
      providerName.setText(provider);
      row.addView(label);
      row.addView(providerName);
      row.addView(enabled);
      row.addView(status);
      layout.addView(row);
    }
  }
  :
}
終了処理

意外に重要だったのが終了処理です。removeUpdates を忘れたまま再び requestLocationUpdates すると onLocationChanged メソッドが呼び出されなくなります。

  @Override
  protected void onDestroy() {
    super.onDestroy();
    // 重要:requestLocationUpdatesしたままアプリを終了すると挙動がおかしくなる。
    locationManager.removeUpdates(this);
    locationManager.removeGpsStatusListener(this);
  }
位置情報取得準備

ここで getProviders ではなく getBestProvider メソッドを使うと正しくプロバイダが取得できないので気をつけましょう。requestLocationUpdates メソッドを呼ぶと位置情報の取得を開始します。

  @Override
  public void onClick(View v) {
    locationManager.removeUpdates(this);
    logText.setText("");
    latText.setText("");
    accText.setText("");
    providerText.setText("");
    locationTimeText.setText("");
    if (v.getId() == R.id.start) {
      EditText timeEdit = (EditText)findViewById(R.id.min_time);
      EditText distanceEdit = (EditText)findViewById(R.id.min_distance);
      int time = Integer.valueOf(timeEdit.getText().toString()) * 1000;
      int distance = Integer.valueOf(distanceEdit.getText().toString());
      List<String> providers = locationManager.getProviders(true);
      for (String provider : providers) {
        locationManager.requestLocationUpdates(provider, time, distance, this);
      }
    }
  }
位置情報取得処理
  @Override
  public void onLocationChanged(Location location) {
    logText.setText(String.valueOf(location.getLongitude()));
    latText.setText(String.valueOf(location.getLatitude()));
    accText.setText(String.valueOf(location.getAccuracy()));
    providerText.setText(String.valueOf(location.getProvider()));
    Date d = new Date(location.getTime());
    locationTimeText.setText(d.toLocaleString());
  }
プロバイダー関連のイベント

プロバイダの状態を通知するイベントです。

  @Override
  public void onProviderDisabled(String provider) {
    LinearLayout row = layoutMap.get(provider);
    TextView tv = (TextView)row.findViewById(ID_LOCATION_PROVIDER_ENABLED);
    tv.setText(PROVIDER_DISABLED);
  }
  @Override
  public void onProviderEnabled(String provider) {
    LinearLayout row = layoutMap.get(provider);
    TextView tv = (TextView)row.findViewById(ID_LOCATION_PROVIDER_ENABLED);
    tv.setText(PROVIDER_ENABLED);
  }
  /**
    * このメソッドは、プロバイダの場所を取得することができない場合、
    * または最近使用不能の期間後に利用可能となっている場合に呼び出されます。
    */
  @Override
  public void onStatusChanged(String provider, int status, Bundle extras) {
    LinearLayout row = layoutMap.get(provider);
    TextView tv = (TextView)row.findViewById(ID_LOCATION_PROVIDER_STATUS);
    String statusString = "Unknown";
    if (status == LocationProvider.AVAILABLE) {
      statusString = "AVAILABLE";
    } else if (status == LocationProvider.OUT_OF_SERVICE) {
      statusString = "OUT OF SERVICE";
    } else if (status == LocationProvider.TEMPORARILY_UNAVAILABLE) {
      statusString = "TEMP UNAVAILABLE";
    }
    tv.setText(statusString);
  }
GPS の状態が通知されるメソッド

このイベントで GPS の状態を知ることができます。このイベントの他にも Android 2.0 で追加された GpsStatus.NmeaListener リスナを使うともっと詳細な情報を取得することができます(情報が多いため別記事で書きます)。

  @Override
  public void onGpsStatusChanged(int event) {
    TextView tv = (TextView)findViewById(R.id.gps_status);
    String status = "";
    if (event == GpsStatus.GPS_EVENT_FIRST_FIX) {
      status = "FIRST FIX";
    } else if (event == GpsStatus.GPS_EVENT_SATELLITE_STATUS) {
      status = "SATELLITE STATUS";
    } else if (event == GpsStatus.GPS_EVENT_STARTED) {
      status = "STARTED";
    } else if (event == GpsStatus.GPS_EVENT_STOPPED) {
      status = "STOPPED";
    }
    tv.setText(status);
  }	

ソースコードGoogle Code で公開しています。

この記事で紹介したプログラムは以下のページで入手できます。Android 1.6 以上で動作します。参考にどうぞ。