アンドロイドで位置情報を取得したい場合は、端末のGPSとWifiや基地局GPSを組み合わせた、FusedLocationProvider が推奨されます。その中でも高い精度(数メール)が得られるのはGPSです。簡単にGPSを起動させて緯度経度を取得してみます。
2024.1.1
LocationManager
API 30 からの変更としては、バックグラウンドでの位置情報へのアクセス権を付与するには、ユーザーをシステム設定に移動して許可を得る必要があります。
GPSの実装は色々と制約がついているので、ここでの内容は以下の条件になります。
- GPSを起動するための簡単な実装
- 1つのRuntime Permissionのみを想定
- フォアグランドのみでの実行
また、基本的にAndroid機はGPSだけでは位置情報を取得していないので FusedLocationProvider を実装することを最初から検討する選択肢もあります。ここは一応勉強のためです。
尚、Kotlinはこちら
GPS簡単な実装
GPSに必要なpermissionは、Android 6.0 Runtime Permission の dangerous permissionに該当するため、ユーザーが許可しないと使えません。これはコードが複雑になりますので、GPSとしての機能を確認するだけの簡単について考えてみます。
registerForActivityResult:
とりあえずpermissionのチェクが許可されない例外はあまり考えないで実装します。
(最終的には例外を考慮する必要があります)
onRequestPermissionsResultは非推奨になり
registerForActivityResult()を使います。
アクティビティの結果を取得する | Android デベロッパー
registerForActivityResult() は、
- ActivityResultContract
- ActivityResultCallback
を受け取り、他のアクティビティを開始するために使用する
- ActivityResultLauncher
を返します。
permissionが既に許可されているかの確認
1 2 3 4 5 6 |
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION); } |
permissionチェックのリクエスト結果を受け取る
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private final ActivityResultLauncher<String> requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), isGranted -> { if (isGranted) { locationStart(); } else { Toast toast = Toast.makeText(this, "これ以上なにもできません", Toast.LENGTH_SHORT); toast.show(); } }); |
Permissionの設定:
Runtime Permissionが出てくる前はこれだけでしたが、AndroidManifestに以下の設定をします。FineとCoarseの2種類あります。Fineを設定する場合は両方必要
1 2 |
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> |
- FineはGPSデバイスを使った精度が高い位置情報を取得します。
- CoarseはGPSが使えない屋内やビルの谷間などでもネットワークを利用して大まかな情報を得ます。
LocationManager
LocationManagerのインスタンス生成:
GPSだけでなく位置情報を管理しているのがLocationMnagerです。そのインスタンスを生成します。
1 |
LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); |
リスナーの登録:
Activityに LocationListener を登録します。
1 2 3 |
public class MainActivity extends AppCompatActivity implements LocationListener { ... } |
requestLocationUpdates:
LocationManagerからrequestLocationUpdatesを使って位置情報をアップデートするメソッドを呼び出します。
1 2 |
locationManager.requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener); |
provider:
-
- GPSを使う場合は LocationManager.GPS_PROVIDER 設定します。
ネットワークの場合は、LocationManager.NETWORK_PROVIDE となります。
ただこの設定の場合はGPS_PROVIDERのみの選択で、ユーザーの位置情報設定が「バッテーリー節約」つまりNETWORK_PROVIDEになっていると設定を促すページに飛ぶようにしてあります。
「高精度」あるいは「端末のみ」の選択です。
また、Criteria を使って、低消費電力モードにすることもできます。Criteria
- GPSを使う場合は LocationManager.GPS_PROVIDER 設定します。
- minTime:最小時間間隔[msec]、これは消費電力に大きく関係します
- minDistance:最小距離間隔[m]、アップデートでの最小距離です
- listener:登録したリスナー
onLocationChanged:
位置情報が変化したケースでonLocationChangedにてgetLatitude()とgetLongitude()を使って緯度経度を取得します。
1 2 3 4 5 6 7 8 9 |
@Override public void onLocationChanged(Location location) { // 緯度 Log.d("debug","Latitude:"+location.getLatitude()); // 経度 Log.d("debug","Longitude:"+location.getLongitude()); } |
サンプルコード
まとめると
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 |
package com.example.testgps; import android.os.Bundle; import androidx.activity.EdgeToEdge; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.widget.TextView; import android.content.Intent; import android.provider.Settings; import android.util.Log; import android.widget.Toast; import android.Manifest; public class MainActivity extends AppCompatActivity implements LocationListener { LocationManager locationManager; private final ActivityResultLauncher<String> requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), isGranted -> { if (isGranted) { locationStart(); } else { Toast toast = Toast.makeText(this, "これ以上なにもできません", Toast.LENGTH_SHORT); toast.show(); } }); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_main); ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); return insets; }); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { requestPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION); } else{ locationStart(); } } private void locationStart(){ Log.d("debug","locationStart()"); // LocationManager インスタンス生成 locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); if (locationManager != null && locationManager.isProviderEnabled( LocationManager.GPS_PROVIDER)) { Log.d("debug", "location manager Enabled"); } else { // GPSを設定するように促す Intent settingsIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); startActivity(settingsIntent); Log.d("debug", "not gpsEnable, startActivity"); } if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION,}, 1000); Log.d("debug", "checkSelfPermission false"); return; } locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 50, this); } @Override public void onLocationChanged(Location location) { // 緯度の表示 TextView textView1 = findViewById(R.id.text_view1); String str1 = "Latitude:"+location.getLatitude(); textView1.setText(str1); // 経度の表示 TextView textView2 = findViewById(R.id.text_view2); String str2 = "Longitude:"+location.getLongitude(); textView2.setText(str2); } @Override public void onProviderEnabled(String provider) { } @Override public void onProviderDisabled(String provider) { } } |
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.testgpssimple"> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <application ... </application> </manifest> |
レイアウトですが、緯度・経度を表示するだけです。
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 |
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/text_view1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="20dp" android:textColor="#44f" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.501" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.1" /> <TextView android:id="@+id/text_view2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20sp" android:textColor="#f44" android:layout_margin="20dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.501" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.2" /> </androidx.constraintlayout.widget.ConstraintLayout> |
emulatorで実行してみます。
最初はpermissionの確認ダイアログが出ますので「アプリの使用時のみ」あるいは「今回のみ」を許可します。
emulatorの右上にあるバーガーアイコン「…」をクリックして
Extended controlsを開きます。
「Location」のタグから地図上の位置、例えば皇居を設定してピンを立てると
住所と緯度経度が下に表示されます
「SAVE POINT」をクリックすると、Saved Pointsにリストアップされ
その下にある
「SET LOCATION」をクリックすると、
アプリに緯度経度が反映されます。
Google Map と連携
GPSの位置情報を取得できても緯度経度だけではあまり意味がありません
地図で見たいわけです。Google Mapで見たいのですが
- GoogleのAPIキーを取得してGoogle Map アプリを作成
- Acitivityのテンプレートがあるのでアプリは難しくない
- APIキーを作成するのは面倒で課金される可能性があるのでテストだけ
Google Mapアプリを作る場合には、プロジェクトの新規作成でMAPアプリを選択すると簡単に出来上がります。
- Intentを使ってGoogle Mapアプリを起動して地図上で位置を確認
- APIキーはいらないIntentで飛ばすという安直なやり方
- ユーザーが自分のアプリに戻ってこないかもしれない
API key 無しでIntentを使って簡単に Google Map を呼び出して地図を表示させる方法です。 GoogleMapを...
緯度経度を、geo:[緯度],[経度]?z=[ズーム] としてGoogleMapアプリに渡すとその位置の地図が表示されます
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 |
@SuppressLint("QueryPermissionsNeeded") private void moveToGMap(Location location){ String str1 = String.valueOf(location.getLatitude()); String str2 = String.valueOf(location.getLongitude()); Uri uri = Uri.parse("geo:"+str1+","+str2+"?z=16"); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); } } @Override public void onLocationChanged(Location location) { // 緯度の表示 TextView textView1 = findViewById(R.id.text_view1); String str1 = "Latitude:"+location.getLatitude(); textView1.setText(str1); // 経度の表示 TextView textView2 = findViewById(R.id.text_view2); String str2 = "Longitude:"+location.getLongitude(); textView2.setText(str2); moveToGMap(location); } |
emulatorで皇居の位置情報をセットしてあるとこのようになりました。
あるいは、Google Mapを使うともっと簡単に調べられます
google.com/maps?q=[緯度],[軽度]
でChromeで調べられます
google.com/maps?q=35.6827,139.7512
35.6827 -> 35°40’57,7″ と変換されているので注意ですが
実機テスト
実機でテストもしてみましょう。
今回はGPSのみの動作ですので、屋外でテストするためにログをTextViewで出せるようにしました。屋外でも、ビルの谷間はほぼあきらめた方がいいです。衛星が見えても1個2個ですから最低3個、通常は4個補足する必要があります。
GPSの位置を取得するまで、数分かかる場合があります。GPS衛星のカレンダーを持っていれば、最初の補足時間が短くなりますが、無い場合は、衛星からその情報を取得するのです。
ソフトウェア開発開発においてはあまり意識することはありませんが、GPSは日常生活でアインシュタインの相対性理論を実感できるものの一つです。
測位では距離計算を電波(光速:30万km/s)を使っていますが、1msecの違いは単純に300Kmになり東京から名古屋あたりまでの距離になります。たらたらアプリで計算なんかできませんね
相対性理論「時間の遅れ」、日常世界で実証
この内容は一番簡単なGPSの実装例です。実際には足りないところを考慮する必要があります。
関連記事:
- FusedLocationProviderClient による位置情報取得
- 複数の権限をリクエストする
- FusedLocationProvider とGoogle Mapで地図を表示
- GPSでの位置情報
- アプリの権限、位置情報をリクエストする実装
References:
LocationManager | Android Developers
Criteria | Android Developers
直近の位置情報を取得する – Android Developers
現在地の更新情報をリクエストする – Android デベロッパー
位置情報の設定を変更する – Android デベロッパー
FusedLocationProviderClient
アプリの権限をリクエストする